From 841839ecd1b0db92708d14caa004194c7cb7258c Mon Sep 17 00:00:00 2001 From: Josh Gum Date: Tue, 12 Sep 2023 05:44:03 -0700 Subject: [PATCH] adds ga4 plugin leveraged from how the fresh site does it --- deno.json | 1 + fresh.config.ts | 8 ++-- plugins/ga4.ts | 117 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 plugins/ga4.ts diff --git a/deno.json b/deno.json index 790480c4e..f9bff25e2 100644 --- a/deno.json +++ b/deno.json @@ -25,6 +25,7 @@ "imports": { "@/": "./", "$fresh/": "https://deno.land/x/fresh@1.4.3/", + "$ga4": "https://raw.githubusercontent.com/denoland/ga4/main/mod.ts", "$gfm": "https://deno.land/x/gfm@0.2.4/mod.ts", "preact": "https://esm.sh/preact@10.15.1", "preact/": "https://esm.sh/preact@10.15.1/", diff --git a/fresh.config.ts b/fresh.config.ts index 5c1422725..adacb4ad3 100644 --- a/fresh.config.ts +++ b/fresh.config.ts @@ -1,13 +1,15 @@ // Copyright 2023 the Deno authors. All rights reserved. MIT license. import twindPlugin from "$fresh/plugins/twindv1.ts"; -import twindConfig from "./twind.config.ts"; +import { FreshOptions } from "$fresh/server.ts"; +import errorHandling from "./plugins/error_handling.ts"; +import ga4 from "./plugins/ga4.ts"; import kvOAuthPlugin from "./plugins/kv_oauth.ts"; import protectedRoutes from "./plugins/protected_routes.ts"; -import errorHandling from "./plugins/error_handling.ts"; -import { FreshOptions } from "$fresh/server.ts"; +import twindConfig from "./twind.config.ts"; export default { plugins: [ + ga4, kvOAuthPlugin, protectedRoutes, twindPlugin(twindConfig), diff --git a/plugins/ga4.ts b/plugins/ga4.ts new file mode 100644 index 000000000..65151ab8c --- /dev/null +++ b/plugins/ga4.ts @@ -0,0 +1,117 @@ +// Copyright 2023 the Deno authors. All rights reserved. MIT license. +import type { MiddlewareHandlerContext, Plugin } from "$fresh/server.ts"; +import type { Event } from "$ga4"; +import { GA4Report, isDocument, isServerError } from "$ga4"; +import type { State } from "@/middleware/session.ts"; + +let showedMissingEnvWarning = false; + +function ga4( + request: Request, + conn: MiddlewareHandlerContext, + response: Response, + _start: number, + error?: unknown, +) { + const GA4_MEASUREMENT_ID = Deno.env.get("GA4_MEASUREMENT_ID"); + + 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 default { + name: "ga4", + middlewares: [ + { + path: "/", + middleware: { + async handler(req, ctx) { + let err; + let res: Response; + const start = performance.now(); + try { + const resp = await ctx.next(); + 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, + ctx as MiddlewareHandlerContext, + res!, + start, + err, + ); + } + }, + }, + }, + ], +} as Plugin;