Skip to content

Commit

Permalink
add: initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Jabolol committed Dec 13, 2023
0 parents commit d6dcf91
Show file tree
Hide file tree
Showing 9 changed files with 283 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Google Analytics tracking ID (Optional)
GA4_MEASUREMENT_ID=

# Fallback image url
FALLBACK_URL=
29 changes: 29 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Deploy
on:
push:
branches: [main]
pull_request:
branches: main

jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read

steps:
- name: Clone repository
uses: actions/checkout@v3

- name: Install Deno
uses: denoland/setup-deno@v1
with:
deno-version: v1.x

- name: Upload to Deno Deploy
uses: denoland/deployctl@v1
with:
project: "brownie"
entrypoint: "./main.ts"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.env
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"deno.enable": true,
"deno.lint": true,
"deno.unstable": true
}
18 changes: 18 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
Copyright 2023 Jabolo <[email protected]>

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the “Software”), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 changes: 16 additions & 0 deletions config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
interface RouteConfig {
routes: Record<string, string>;
}

/**
* Route mapping configuration object.
* The convention is to use brownie ingredients as paths.
*/
export const config: RouteConfig = {
routes: {
cocoa:
"https://jabolo-stats.vercel.app/api?username=Jabolol&theme=dracula&hide_border=false&include_all_commits=false&count_private=true&show_icons=true",
vanilla:
"https://github-readme-streak-stats-eight-iota.vercel.app/?user=Jabolol&theme=dracula&hide_border=false",
},
};
11 changes: 11 additions & 0 deletions deno.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"lock": false,
"tasks": {
"start": "deno run --watch --env --allow-env=GA4_MEASUREMENT_ID --allow-net --unstable main.ts"
},
"imports": {
"fp-ts": "npm:fp-ts",
"ga4": "https://raw.githubusercontent.com/denoland/ga4/04a1ce209116f158b5ef1658b957bdb109db68ed/mod.ts",
"~/": "./"
}
}
76 changes: 76 additions & 0 deletions main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { report } from "~/utils.ts";
import { config } from "~/config.ts";
import { function as func, option, task, taskEither } from "fp-ts";

/**
* Fetches the bytes from the specified URL.
* @param url - The URL to fetch the bytes from.
* @returns A promise that resolves to an ArrayBuffer.
*/
const fetchBytes = async (url: string) => {
const response = await fetch(url);
if (!response.ok) throw new Error("Failed to fetch resource");
return response.arrayBuffer();
};

/**
* Retrieves the value from the config.routes object based on the request URL.
* If the value is not found, it falls back to the value of the `FALLBACK_URL` environment variable.
* @param request - The request object.
* @returns The retrieved value or the fallback URL.
*/
const retrieve = func.flow(
taskEither.tryCatchK(
fetchBytes,
(reason) => new Error(String(reason)),
),
);

/**
* Retrieves the path of the request URL given the request object.
* @param request - The request object.
* @returns The path of the request URL.
*/
const path = func.flow(
(request: Request) => new URL(request.url),
(url) => url.pathname,
(pathname) => pathname.slice(1),
(pathname) => config.routes[pathname],
option.fromNullable,
option.getOrElse(() => Deno.env.get("FALLBACK_URL")!),
);

/**
* Builds a task that transforms the input taskEither into a task that resolves to a Response object.
* @param input - The input taskEither.
* @returns A task that resolves to a Response object.
*/
const build = (
input: taskEither.TaskEither<Error, ArrayBuffer>,
) =>
func.pipe(
input,
taskEither.map<ArrayBuffer, Response>((bytes) =>
new Response(bytes, { headers: { "Content-Type": "image/svg+xml" } })
),
taskEither.getOrElse<Error, Response>((error) =>
task.of(new Response(error.message))
),
);

/**
* Handles the incoming request and returns a task that reports the request and response.
* @param request - The incoming request object.
* @param ctx - The Deno.ServeHandlerInfo object.
* @returns A task that reports the request and response.
*/
const handler = (request: Request, ctx: Deno.ServeHandlerInfo) =>
func.pipe(
request,
path,
retrieve,
build,
task.map((response) => report(request, response, ctx)),
)();

Deno.serve(handler);
122 changes: 122 additions & 0 deletions utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* @file Google Analytics 4 reporting utility
* @description This file is based on the original work from Deno's ga4 project.
* For details, see: {@link https://github.com/denoland/ga4/blob/main/mod.ts}
*
* @modifications
* - Removed deprecated interface `ConnInfo`
* - Refactored `handler` to make it fresh-agnostic
*
* @license MIT The original work is licensed under the MIT License.
* {@link https://github.com/denoland/ga4/blob/main/LICENSE.md}
*
* Additional terms:
* © Jabolo 2023
* Licensed under the terms of the MIT License.
*/
import { Event, GA4Report, isDocument, isServerError } from "ga4";

const GA4_MEASUREMENT_ID = Deno.env.get("GA4_MEASUREMENT_ID");

let showedMissingEnvWarning = false;

function ga4(
request: Request,
conn: Deno.ServeHandlerInfo,
response: Response,
_start: number,
error?: unknown,
) {
if (GA4_MEASUREMENT_ID === undefined) {
if (!showedMissingEnvWarning) {
showedMissingEnvWarning = true;
console.warn(
"GA4_MEASUREMENT_ID environment variable not set. Google Analytics reporting disabled.",
);
}
return;
}
Promise.resolve().then(async () => {
// We're tracking page views and file downloads. These are the only two
// HTTP methods that _might_ be used.
if (!/^(GET|POST)$/.test(request.method)) {
return;
}

// If the visitor is using a web browser, only create events when we serve
// a top level documents or download; skip assets like css, images, fonts.
if (!isDocument(request, response) && error == null) {
return;
}

let event: Event | null = null;
const contentType = response.headers.get("content-type");
if (/text\/html/.test(contentType!)) {
event = { name: "page_view", params: {} }; // Probably an old browser.
}

if (event == null && error == null) {
return;
}

// If an exception was thrown, build a separate event to report it.
let exceptionEvent;
if (error != null) {
exceptionEvent = {
name: "exception",
params: {
description: String(error),
fatal: isServerError(response),
},
};
} else {
exceptionEvent = undefined;
}

// Create basic report.
const measurementId = GA4_MEASUREMENT_ID;
// @ts-ignore GA4Report doesn't even use the localAddress parameter
const report = new GA4Report({ measurementId, request, response, conn });

// Override the default (page_view) event.
report.event = event;

// Add the exception event, if any.
if (exceptionEvent != null) {
report.events.push(exceptionEvent);
}

await report.send();
}).catch((err) => {
console.error(`Internal error: ${err}`);
});
}

export function report(
req: Request,
resp: Response,
conn: Deno.ServeHandlerInfo,
): Response {
let err;
let res: Response;
const start = performance.now();
try {
const headers = new Headers(resp.headers);
res = new Response(resp.body, { status: resp.status, headers });
return res;
} catch (e) {
res = new Response("Internal Server Error", {
status: 500,
});
err = e;
throw e;
} finally {
ga4(
req,
conn,
res!,
start,
err,
);
}
}

0 comments on commit d6dcf91

Please sign in to comment.