From 32a9db78e8ed51b6474f3a0c82e7cd2b7883750a Mon Sep 17 00:00:00 2001 From: Mackenly Jones <53151058+mackenly@users.noreply.github.com> Date: Mon, 11 Nov 2024 22:01:59 +0000 Subject: [PATCH 1/2] Adds serverside route to v2 --- app/analytics/__tests__/collect.test.ts | 23 +++ app/analytics/collect.ts | 177 +++++++++++++++++------- 2 files changed, 148 insertions(+), 52 deletions(-) diff --git a/app/analytics/__tests__/collect.test.ts b/app/analytics/__tests__/collect.test.ts index 2bb47d9a..dd31ba63 100644 --- a/app/analytics/__tests__/collect.test.ts +++ b/app/analytics/__tests__/collect.test.ts @@ -3,6 +3,7 @@ import { Mock, describe, expect, test, vi, beforeEach } from "vitest"; import httpMocks from "node-mocks-http"; import { collectRequestHandler } from "../collect"; +import { AnalyticsEngineDataset } from "@cloudflare/workers-types"; const defaultRequestParams = generateRequestParams({ "user-agent": @@ -216,4 +217,26 @@ describe("collectRequestHandler", () => { ], ); }); + + test("a PATCH request should return 405", () => { + const env = { + WEB_COUNTER_AE: { + writeDataPoint: vi.fn(), + } as AnalyticsEngineDataset, + } as Env; + + const request = httpMocks.createRequest({ + method: "PATCH", + url: "https://example.com", + // Cloudflare-specific request properties + cf: { + country: "US", + }, + }); + + const response = collectRequestHandler(request as any, env); + + expect(response.status).toBe(405); + expect(response.statusText).toBe("Method not allowed"); + }); }); diff --git a/app/analytics/collect.ts b/app/analytics/collect.ts index 7dd059f2..c451d1d5 100644 --- a/app/analytics/collect.ts +++ b/app/analytics/collect.ts @@ -52,60 +52,133 @@ function extractParamsFromQueryString(requestUrl: string): { } export function collectRequestHandler(request: Request, env: Env) { - const params = extractParamsFromQueryString(request.url); - - const userAgent = request.headers.get("user-agent") || undefined; - const parsedUserAgent = new UAParser(userAgent); - - parsedUserAgent.getBrowser().name; - - const { newVisitor, newSession } = checkVisitorSession( - request.headers.get("if-modified-since"), - ); - - const data: DataPoint = { - siteId: params.sid, - host: params.h, - path: params.p, - referrer: params.r, - newVisitor: newVisitor ? 1 : 0, - newSession: newSession ? 1 : 0, - // user agent stuff - userAgent: userAgent, - browserName: parsedUserAgent.getBrowser().name, - deviceModel: parsedUserAgent.getDevice().model, - }; - - // NOTE: location is derived from Cloudflare-specific request properties - // see: https://developers.cloudflare.com/workers/runtime-apis/request/#incomingrequestcfproperties - const country = (request as RequestInit).cf?.country; - if (typeof country === "string") { - data.country = country; - } - - writeDataPoint(env.WEB_COUNTER_AE, data); - - // encode 1x1 transparent gif - const gif = "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"; - const gifData = atob(gif); - const gifLength = gifData.length; - const arrayBuffer = new ArrayBuffer(gifLength); - const uintArray = new Uint8Array(arrayBuffer); - for (let i = 0; i < gifLength; i++) { - uintArray[i] = gifData.charCodeAt(i); + switch (request.method) { + case "GET": { + const params = extractParamsFromQueryString(request.url); + + const userAgent = request.headers.get("user-agent") || undefined; + const parsedUserAgent = new UAParser(userAgent); + + parsedUserAgent.getBrowser().name; + + const { newVisitor, newSession } = checkVisitorSession( + request.headers.get("if-modified-since"), + ); + + const data: DataPoint = { + siteId: params.sid, + host: params.h, + path: params.p, + referrer: params.r, + newVisitor: newVisitor ? 1 : 0, + newSession: newSession ? 1 : 0, + // user agent stuff + userAgent: userAgent, + browserName: parsedUserAgent.getBrowser().name, + deviceModel: parsedUserAgent.getDevice().model, + }; + + // NOTE: location is derived from Cloudflare-specific request properties + // see: https://developers.cloudflare.com/workers/runtime-apis/request/#incomingrequestcfproperties + const country = (request as RequestInit).cf?.country; + if (typeof country === "string") { + data.country = country; + } + + writeDataPoint(env.WEB_COUNTER_AE, data); + + // encode 1x1 transparent gif + const gif = + "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"; + const gifData = atob(gif); + const gifLength = gifData.length; + const arrayBuffer = new ArrayBuffer(gifLength); + const uintArray = new Uint8Array(arrayBuffer); + for (let i = 0; i < gifLength; i++) { + uintArray[i] = gifData.charCodeAt(i); + } + + return new Response(arrayBuffer, { + headers: { + "Content-Type": "image/gif", + Expires: "Mon, 01 Jan 1990 00:00:00 GMT", + "Cache-Control": "no-cache", + Pragma: "no-cache", + "Last-Modified": new Date().toUTCString(), + Tk: "N", // not tracking + }, + status: 200, + }); + } + case "POST": { + const body: PostRequestBody = + request.json() as unknown as PostRequestBody; + if (!body) { + return new Response("Invalid request", { status: 400 }); + } + + if ( + !body.siteId || + !body.host || + !body.userAgent || + !body.path || + !body.country || + !body.referrer || + !body.browserName || + !body.deviceModel || + !body.ifModifiedSince + ) { + return new Response("Missing required fields", { status: 400 }); + } + + const userAgent = body.userAgent || undefined; + const parsedUserAgent = new UAParser(userAgent); + + parsedUserAgent.getBrowser().name; + + const { newVisitor, newSession } = checkVisitorSession( + body.ifModifiedSince, + ); + + const data: DataPoint = { + siteId: body.siteId, + host: body.host, + path: body.path, + country: body.country, + referrer: body.referrer, + newVisitor: newVisitor ? 1 : 0, + newSession: newSession ? 1 : 0, + // user agent stuff + userAgent: userAgent, + browserName: + body.browserName || parsedUserAgent.getBrowser().name, + deviceModel: + body.deviceModel || parsedUserAgent.getDevice().model, + }; + + writeDataPoint(env.WEB_COUNTER_AE, data); + + return new Response("OK", { status: 200 }); + } + default: { + return new Response("Method not allowed", { + status: 405, + statusText: "Method not allowed", + }); + } } +} - return new Response(arrayBuffer, { - headers: { - "Content-Type": "image/gif", - Expires: "Mon, 01 Jan 1990 00:00:00 GMT", - "Cache-Control": "no-cache", - Pragma: "no-cache", - "Last-Modified": new Date().toUTCString(), - Tk: "N", // not tracking - }, - status: 200, - }); +interface PostRequestBody { + siteId: string; + host: string; + userAgent: string; + path: string; + country: string; + referrer: string; + browserName: string; + deviceModel: string; + ifModifiedSince: string; } interface DataPoint { From bf4ade165411b9be2abbc1b0c66d92bd44517e00 Mon Sep 17 00:00:00 2001 From: Mackenly Jones <53151058+mackenly@users.noreply.github.com> Date: Mon, 11 Nov 2024 23:13:11 +0000 Subject: [PATCH 2/2] Fixs field names --- app/analytics/collect.ts | 62 +++++++++++++++------------------------- 1 file changed, 23 insertions(+), 39 deletions(-) diff --git a/app/analytics/collect.ts b/app/analytics/collect.ts index c451d1d5..3b2a66e6 100644 --- a/app/analytics/collect.ts +++ b/app/analytics/collect.ts @@ -117,43 +117,27 @@ export function collectRequestHandler(request: Request, env: Env) { return new Response("Invalid request", { status: 400 }); } - if ( - !body.siteId || - !body.host || - !body.userAgent || - !body.path || - !body.country || - !body.referrer || - !body.browserName || - !body.deviceModel || - !body.ifModifiedSince - ) { - return new Response("Missing required fields", { status: 400 }); - } - - const userAgent = body.userAgent || undefined; + const userAgent = + body.ua || request.headers.get("user-agent") || undefined; const parsedUserAgent = new UAParser(userAgent); - parsedUserAgent.getBrowser().name; - const { newVisitor, newSession } = checkVisitorSession( - body.ifModifiedSince, - ); + const ifModifiedSince = + body.ims || request.headers.get("if-modified-since"); + const { newVisitor, newSession } = + checkVisitorSession(ifModifiedSince); const data: DataPoint = { - siteId: body.siteId, - host: body.host, - path: body.path, - country: body.country, - referrer: body.referrer, + siteId: body.sid, + host: body.h, + userAgent: body.ua, + path: body.p, + country: body.c, + referrer: body.r, + browserName: body.bn || parsedUserAgent.getBrowser().name, + deviceModel: body.dm || parsedUserAgent.getDevice().model, newVisitor: newVisitor ? 1 : 0, newSession: newSession ? 1 : 0, - // user agent stuff - userAgent: userAgent, - browserName: - body.browserName || parsedUserAgent.getBrowser().name, - deviceModel: - body.deviceModel || parsedUserAgent.getDevice().model, }; writeDataPoint(env.WEB_COUNTER_AE, data); @@ -170,15 +154,15 @@ export function collectRequestHandler(request: Request, env: Env) { } interface PostRequestBody { - siteId: string; - host: string; - userAgent: string; - path: string; - country: string; - referrer: string; - browserName: string; - deviceModel: string; - ifModifiedSince: string; + sid?: string; // site id + h?: string; // host + ua?: string; // user agent + p?: string; // path + c?: string; // country + r?: string; // referrer + bn?: string; // browser name + dm?: string; // device model + ims?: string; // if modified since } interface DataPoint {