From 2eb173bb754edd44a3274c691938c6ab3ab577bc Mon Sep 17 00:00:00 2001 From: Eric Clemmons Date: Wed, 15 Jul 2026 23:02:36 -0500 Subject: [PATCH 1/2] Emit Worker version IDs in asset analytics --- .changeset/assets-script-version-analytics.md | 7 +++++++ packages/workers-shared/asset-worker/src/analytics.ts | 3 +++ packages/workers-shared/asset-worker/src/configuration.ts | 1 + packages/workers-shared/asset-worker/src/worker.ts | 2 ++ .../workers-shared/asset-worker/tests/analytics.test.ts | 2 ++ packages/workers-shared/utils/types.ts | 1 + 6 files changed, 16 insertions(+) create mode 100644 .changeset/assets-script-version-analytics.md diff --git a/.changeset/assets-script-version-analytics.md b/.changeset/assets-script-version-analytics.md new file mode 100644 index 0000000000..5257fb9275 --- /dev/null +++ b/.changeset/assets-script-version-analytics.md @@ -0,0 +1,7 @@ +--- +"@cloudflare/workers-shared": minor +--- + +Emit customer Worker version IDs in Asset Worker analytics + +Asset Worker request events can now include the customer Worker version UUID supplied by the assets pipeline. This enables analytics consumers to filter static asset metrics by the versions selected in the Workers dashboard without changing the existing Asset Worker service version field. diff --git a/packages/workers-shared/asset-worker/src/analytics.ts b/packages/workers-shared/asset-worker/src/analytics.ts index d16fc0fad8..14c99c71a2 100644 --- a/packages/workers-shared/asset-worker/src/analytics.ts +++ b/packages/workers-shared/asset-worker/src/analytics.ts @@ -88,6 +88,8 @@ type Data = { servedBy?: ServedBy; // blob11 - Whether the request was a navigation or a subresource requestKind?: RequestKind; + // blob12 - The customer Worker version UUID associated with this asset pipeline + scriptVersionId?: string; }; const COMPATIBILITY_FLAG_MASKS: Record = @@ -155,6 +157,7 @@ export class Analytics { this.data.cohort, // blob9 this.data.servedBy, // blob10 this.data.requestKind, // blob11 + this.data.scriptVersionId, // blob12 ], }); } diff --git a/packages/workers-shared/asset-worker/src/configuration.ts b/packages/workers-shared/asset-worker/src/configuration.ts index c39102fdf7..de6cb33a4c 100644 --- a/packages/workers-shared/asset-worker/src/configuration.ts +++ b/packages/workers-shared/asset-worker/src/configuration.ts @@ -23,6 +23,7 @@ export const normalizeConfiguration = ( has_static_routing: configuration?.has_static_routing ?? false, account_id: configuration?.account_id ?? -1, script_id: configuration?.script_id ?? -1, + script_version_id: configuration?.script_version_id ?? "", debug: configuration?.debug ?? false, }; }; diff --git a/packages/workers-shared/asset-worker/src/worker.ts b/packages/workers-shared/asset-worker/src/worker.ts index 5250f09a84..f1064df5e8 100644 --- a/packages/workers-shared/asset-worker/src/worker.ts +++ b/packages/workers-shared/asset-worker/src/worker.ts @@ -311,6 +311,7 @@ async function runFetchRequest( entrypoint: EntrypointType.Inner, cohort: cohort ?? "unknown", requestKind: getRequestKind(request), + scriptVersionId: config.script_version_id || undefined, }); } @@ -429,6 +430,7 @@ export default class AssetWorkerOuter version: this.env.VERSION_METADATA.tag, entrypoint: EntrypointType.Outer, requestKind: getRequestKind(request), + scriptVersionId: this.env.CONFIG.script_version_id || undefined, }); } sentry = setupSentry( diff --git a/packages/workers-shared/asset-worker/tests/analytics.test.ts b/packages/workers-shared/asset-worker/tests/analytics.test.ts index 00ebd426da..0780cbdeac 100644 --- a/packages/workers-shared/asset-worker/tests/analytics.test.ts +++ b/packages/workers-shared/asset-worker/tests/analytics.test.ts @@ -59,6 +59,7 @@ describe("[Asset Worker] Analytics", () => { cohort: "free", servedBy: "asset", requestKind: "navigation", + scriptVersionId: "01234567-89ab-cdef-0123-456789abcdef", }); analytics.write(); @@ -85,6 +86,7 @@ describe("[Asset Worker] Analytics", () => { expect(event?.blobs?.[8]).toBe("free"); // cohort expect(event?.blobs?.[9]).toBe("asset"); // servedBy expect(event?.blobs?.[10]).toBe("navigation"); // requestKind + expect(event?.blobs?.[11]).toBe("01234567-89ab-cdef-0123-456789abcdef"); // scriptVersionId // Indexes expect(event?.accountId).toBe(123); diff --git a/packages/workers-shared/utils/types.ts b/packages/workers-shared/utils/types.ts index 1cd4374b3c..1cd2f0a84a 100644 --- a/packages/workers-shared/utils/types.ts +++ b/packages/workers-shared/utils/types.ts @@ -67,6 +67,7 @@ export const HeadersSchema = z .optional(); export const AssetConfigSchema = z.object({ + script_version_id: z.string().optional(), compatibility_date: z.string().optional(), compatibility_flags: z.array(z.string()).optional(), html_handling: z From b92557cab6631291d932069106db4f1dcf607386 Mon Sep 17 00:00:00 2001 From: Eric Clemmons Date: Thu, 16 Jul 2026 10:58:22 -0500 Subject: [PATCH 2/2] Split customer-facing asset analytics --- .changeset/assets-script-version-analytics.md | 4 +- .../asset-worker/src/analytics.ts | 3 - .../asset-worker/src/customer-analytics.ts | 60 +++++++++++++++++++ .../workers-shared/asset-worker/src/worker.ts | 16 ++++- .../asset-worker/tests/analytics.test.ts | 2 - .../tests/customer-analytics.test.ts | 46 ++++++++++++++ 6 files changed, 123 insertions(+), 8 deletions(-) create mode 100644 packages/workers-shared/asset-worker/src/customer-analytics.ts create mode 100644 packages/workers-shared/asset-worker/tests/customer-analytics.test.ts diff --git a/.changeset/assets-script-version-analytics.md b/.changeset/assets-script-version-analytics.md index 5257fb9275..34777aff3e 100644 --- a/.changeset/assets-script-version-analytics.md +++ b/.changeset/assets-script-version-analytics.md @@ -2,6 +2,6 @@ "@cloudflare/workers-shared": minor --- -Emit customer Worker version IDs in Asset Worker analytics +Emit customer-facing Asset Worker request analytics separately -Asset Worker request events can now include the customer Worker version UUID supplied by the assets pipeline. This enables analytics consumers to filter static asset metrics by the versions selected in the Workers dashboard without changing the existing Asset Worker service version field. +Asset Worker requests now write a separate customer-facing analytics event that includes the customer Worker version UUID supplied by the assets pipeline. This enables analytics consumers to filter static asset metrics by the versions selected in the Workers dashboard without exposing operational Asset Worker fields. diff --git a/packages/workers-shared/asset-worker/src/analytics.ts b/packages/workers-shared/asset-worker/src/analytics.ts index 14c99c71a2..d16fc0fad8 100644 --- a/packages/workers-shared/asset-worker/src/analytics.ts +++ b/packages/workers-shared/asset-worker/src/analytics.ts @@ -88,8 +88,6 @@ type Data = { servedBy?: ServedBy; // blob11 - Whether the request was a navigation or a subresource requestKind?: RequestKind; - // blob12 - The customer Worker version UUID associated with this asset pipeline - scriptVersionId?: string; }; const COMPATIBILITY_FLAG_MASKS: Record = @@ -157,7 +155,6 @@ export class Analytics { this.data.cohort, // blob9 this.data.servedBy, // blob10 this.data.requestKind, // blob11 - this.data.scriptVersionId, // blob12 ], }); } diff --git a/packages/workers-shared/asset-worker/src/customer-analytics.ts b/packages/workers-shared/asset-worker/src/customer-analytics.ts new file mode 100644 index 0000000000..b67a6e531b --- /dev/null +++ b/packages/workers-shared/asset-worker/src/customer-analytics.ts @@ -0,0 +1,60 @@ +import type { ReadyAnalytics } from "./types"; + +const VERSION = 1; + +type Data = { + accountId?: number; + scriptId?: number; + coloId?: number; + status?: number; + hostname?: string; + cacheStatus?: string; + scriptVersionId?: string; +}; + +/** + * Customer-facing request analytics. Keep this payload limited to fields that + * are intentionally exposed through the Workers Analytics GraphQL API. + */ +export class CustomerAnalytics { + private data: Data = {}; + + constructor(private readyAnalytics?: ReadyAnalytics) {} + + setData(newData: Partial) { + this.data = { ...this.data, ...newData }; + } + + write() { + if (!this.readyAnalytics) { + return; + } + + this.readyAnalytics.logEvent({ + version: VERSION, + accountId: this.data.accountId, + indexId: this.data.scriptId?.toString(), + doubles: [ + undefined, // double1 + this.data.coloId, // double2 + undefined, // double3 + undefined, // double4 + this.data.status, // double5 + ], + blobs: [ + this.data.hostname?.substring(0, 256), // blob1 + undefined, // blob2 + undefined, // blob3 + undefined, // blob4 + undefined, // blob5 + undefined, // blob6 + undefined, // blob7 + this.data.cacheStatus, // blob8 + undefined, // blob9 + undefined, // blob10 + undefined, // blob11 + this.data.scriptVersionId, // blob12 + ], + }); + } +} diff --git a/packages/workers-shared/asset-worker/src/worker.ts b/packages/workers-shared/asset-worker/src/worker.ts index f1064df5e8..5ede80ca6d 100644 --- a/packages/workers-shared/asset-worker/src/worker.ts +++ b/packages/workers-shared/asset-worker/src/worker.ts @@ -5,6 +5,7 @@ import { mockJaegerBinding } from "../../utils/tracing"; import { Analytics, EntrypointType, getRequestKind } from "./analytics"; import { AssetsManifest } from "./assets-manifest"; import { normalizeConfiguration } from "./configuration"; +import { CustomerAnalytics } from "./customer-analytics"; import { ExperimentAnalytics } from "./experiment-analytics"; import { canFetch, handleRequest } from "./handler"; import { handleError, submitMetrics } from "./utils/final-operations"; @@ -47,6 +48,7 @@ export type Env = { ENVIRONMENT: Environment; EXPERIMENT_ANALYTICS: ReadyAnalytics; ANALYTICS: ReadyAnalytics; + CUSTOMER_ANALYTICS: ReadyAnalytics; COLO_METADATA: ColoMetadata; UNSAFE_PERFORMANCE: UnsafePerformanceTimer; VERSION_METADATA: WorkerVersionMetadata; @@ -264,6 +266,7 @@ async function runFetchRequest( ): Promise { let sentry: ReturnType | undefined; const analytics = new Analytics(env.ANALYTICS); + const customerAnalytics = new CustomerAnalytics(env.CUSTOMER_ANALYTICS); const performance = new PerformanceTimer(env.UNSAFE_PERFORMANCE); const startTimeMs = performance.now(); @@ -311,6 +314,12 @@ async function runFetchRequest( entrypoint: EntrypointType.Inner, cohort: cohort ?? "unknown", requestKind: getRequestKind(request), + }); + customerAnalytics.setData({ + accountId: config.account_id, + scriptId: config.script_id, + coloId: env.COLO_METADATA.coloId, + hostname: url.hostname, scriptVersionId: config.script_version_id || undefined, }); } @@ -333,12 +342,18 @@ async function runFetchRequest( ); analytics.setData({ status: response.status }); + customerAnalytics.setData({ + status: response.status, + cacheStatus: analytics.getData("cacheStatus"), + }); return response; }); } catch (err) { + customerAnalytics.setData({ status: 500 }); return handleError(sentry, analytics, err); } finally { + customerAnalytics.write(); submitMetrics(analytics, performance, startTimeMs); } } @@ -430,7 +445,6 @@ export default class AssetWorkerOuter version: this.env.VERSION_METADATA.tag, entrypoint: EntrypointType.Outer, requestKind: getRequestKind(request), - scriptVersionId: this.env.CONFIG.script_version_id || undefined, }); } sentry = setupSentry( diff --git a/packages/workers-shared/asset-worker/tests/analytics.test.ts b/packages/workers-shared/asset-worker/tests/analytics.test.ts index 0780cbdeac..00ebd426da 100644 --- a/packages/workers-shared/asset-worker/tests/analytics.test.ts +++ b/packages/workers-shared/asset-worker/tests/analytics.test.ts @@ -59,7 +59,6 @@ describe("[Asset Worker] Analytics", () => { cohort: "free", servedBy: "asset", requestKind: "navigation", - scriptVersionId: "01234567-89ab-cdef-0123-456789abcdef", }); analytics.write(); @@ -86,7 +85,6 @@ describe("[Asset Worker] Analytics", () => { expect(event?.blobs?.[8]).toBe("free"); // cohort expect(event?.blobs?.[9]).toBe("asset"); // servedBy expect(event?.blobs?.[10]).toBe("navigation"); // requestKind - expect(event?.blobs?.[11]).toBe("01234567-89ab-cdef-0123-456789abcdef"); // scriptVersionId // Indexes expect(event?.accountId).toBe(123); diff --git a/packages/workers-shared/asset-worker/tests/customer-analytics.test.ts b/packages/workers-shared/asset-worker/tests/customer-analytics.test.ts new file mode 100644 index 0000000000..ebea6e5657 --- /dev/null +++ b/packages/workers-shared/asset-worker/tests/customer-analytics.test.ts @@ -0,0 +1,46 @@ +import { describe, it, vi } from "vitest"; +import { CustomerAnalytics } from "../src/customer-analytics"; +import type { ReadyAnalyticsEvent } from "../src/types"; + +describe("[Asset Worker] Customer Analytics", () => { + it("writes only the customer-facing request fields", ({ expect }) => { + let captured: ReadyAnalyticsEvent | undefined; + const analytics = new CustomerAnalytics({ + logEvent: vi.fn((event: ReadyAnalyticsEvent) => { + captured = event; + }), + }); + + analytics.setData({ + accountId: 123, + scriptId: 456, + coloId: 789, + status: 200, + hostname: "example.com", + cacheStatus: "HIT", + scriptVersionId: "01234567-89ab-cdef-0123-456789abcdef", + }); + analytics.write(); + + expect(captured).toEqual({ + version: 1, + accountId: 123, + indexId: "456", + doubles: [undefined, 789, undefined, undefined, 200], + blobs: [ + "example.com", + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + "HIT", + undefined, + undefined, + undefined, + "01234567-89ab-cdef-0123-456789abcdef", + ], + }); + }); +});