-
Notifications
You must be signed in to change notification settings - Fork 0
Prerender content pages and drop client-side zod #30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| import type { ExecutionContext } from "@cloudflare/workers-types"; | ||
| import type { Env } from "./env"; | ||
| import { AI_IKEY, AI_INGESTION_ENDPOINT, resolveEnvironment } from "../telemetry-config"; | ||
|
|
||
| interface Envelope { | ||
| name: string; | ||
|
|
@@ -9,39 +9,8 @@ interface Envelope { | |
| data: { baseType: string; baseData: Record<string, unknown> }; | ||
| } | ||
|
|
||
| interface ParsedConnString { | ||
| iKey: string; | ||
| ingestionEndpoint: string; | ||
| } | ||
|
|
||
| const SDK_VERSION = "decipher-ms:1.0"; | ||
|
|
||
| function resolveEnvironment(hostname: string): string { | ||
| if (hostname === "decipher.ms" || hostname === "www.decipher.ms") return "production"; | ||
| const previewMatch = hostname.match(/^([a-z0-9-]+)-decipher-ms\.[^.]+\.workers\.dev$/i); | ||
| if (previewMatch) return `preview:${previewMatch[1]}`; | ||
| if (hostname.endsWith(".workers.dev")) return "preview"; | ||
| return "development"; | ||
| } | ||
|
|
||
| function parseConnString(s: unknown): ParsedConnString { | ||
| if (typeof s !== "string" || s.length === 0) { | ||
| throw new Error("APPLICATIONINSIGHTS_CONNECTION_STRING is empty or not a string"); | ||
| } | ||
| const map: Record<string, string> = {}; | ||
| for (const part of s.split(";")) { | ||
| const eq = part.indexOf("="); | ||
| if (eq < 0) continue; | ||
| map[part.slice(0, eq).trim().toLowerCase()] = part.slice(eq + 1).trim(); | ||
| } | ||
| const iKey = map["instrumentationkey"]; | ||
| const ingestionEndpoint = (map["ingestionendpoint"] || "").replace(/\/$/, ""); | ||
| if (!iKey || !ingestionEndpoint) { | ||
| throw new Error("APPLICATIONINSIGHTS_CONNECTION_STRING missing iKey or IngestionEndpoint"); | ||
| } | ||
| return { iKey, ingestionEndpoint }; | ||
| } | ||
|
|
||
| function randomHex(bytes: number): string { | ||
| const arr = new Uint8Array(bytes); | ||
| crypto.getRandomValues(arr); | ||
|
|
@@ -63,7 +32,6 @@ function formatDuration(ms: number): string { | |
|
|
||
| export class Telemetry { | ||
| private envelopes: Envelope[] = []; | ||
| private readonly conn: ParsedConnString | null; | ||
| private readonly requestProperties: Record<string, string> = {}; | ||
| readonly operationId = randomHex(16); | ||
|
|
||
|
|
@@ -83,35 +51,22 @@ export class Telemetry { | |
| private readonly cloudRole: string; | ||
|
|
||
| constructor( | ||
| env: Env, | ||
| private readonly ctx: ExecutionContext, | ||
| hostname: string, | ||
| ) { | ||
| this.environment = resolveEnvironment(hostname); | ||
| this.cloudRole = `decipher-ms-${this.environment}`; | ||
| try { | ||
| this.conn = parseConnString(env.APPLICATIONINSIGHTS_CONNECTION_STRING); | ||
| } catch (err) { | ||
| console.error("Telemetry disabled:", err); | ||
| this.conn = null; | ||
| } | ||
| } | ||
|
|
||
| clientConfig(): { iKey: string; ingestionEndpoint: string; environment: string } | null { | ||
| if (!this.conn) return null; | ||
| return { | ||
| iKey: this.conn.iKey, | ||
| ingestionEndpoint: this.conn.ingestionEndpoint, | ||
| environment: this.environment, | ||
| }; | ||
| } | ||
|
|
||
| newSpanId(): string { | ||
| return randomHex(8); | ||
| } | ||
|
|
||
| private push(baseType: string, baseData: Record<string, unknown>, parentId?: string): void { | ||
| if (!this.conn) return; | ||
| // Don't emit from local dev (no real hostname): keeps localhost preview from | ||
| // POSTing to the shared App Insights ingestion endpoint, matching the prior | ||
| // "disabled without a connection string" behavior. | ||
| if (this.environment === "development") return; | ||
| const tags: Record<string, string> = { | ||
| "ai.operation.id": this.operationId, | ||
| "ai.cloud.role": this.cloudRole, | ||
|
|
@@ -121,7 +76,7 @@ export class Telemetry { | |
| this.envelopes.push({ | ||
| name: `Microsoft.ApplicationInsights.${baseType.replace(/Data$/, "")}`, | ||
| time: new Date().toISOString(), | ||
| iKey: this.conn.iKey, | ||
| iKey: AI_IKEY, | ||
| tags, | ||
| data: { baseType, baseData: { ver: 2, ...baseData } }, | ||
| }); | ||
|
|
@@ -207,10 +162,10 @@ export class Telemetry { | |
| } | ||
|
|
||
| async flushNow(): Promise<void> { | ||
| if (!this.conn || this.envelopes.length === 0) return; | ||
| if (this.envelopes.length === 0) return; | ||
| const payload = this.envelopes.map((e) => JSON.stringify(e)).join("\n"); | ||
| this.envelopes = []; | ||
| const url = `${this.conn.ingestionEndpoint}/v2.1/track`; | ||
| const url = `${AI_INGESTION_ENDPOINT}/v2.1/track`; | ||
| try { | ||
|
Comment on lines
164
to
169
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed — same fix server-side in c6ed36e: |
||
| const res = await fetch(url, { | ||
| method: "POST", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| // App Insights client identifiers, shared by the browser RUM client | ||
| // (telemetry.ts) and the server request tracer (server/telemetry.ts). | ||
| // | ||
| // These are NOT secret: the instrumentation key and ingestion endpoint are | ||
| // shipped to every visitor in the client telemetry payload (visible in | ||
| // view-source), so a "connection string secret" only ever protected public | ||
| // values. Keeping them in source lets prerendered (static) pages configure | ||
| // telemetry with zero per-request server work — no SSR round-trip just to | ||
| // learn an iKey that is already public. | ||
| // | ||
| // The `environment` dimension is resolved from the request/location hostname | ||
| // at runtime (below), so a single build serves production and preview Workers | ||
| // while still tagging telemetry correctly. | ||
| export const AI_IKEY = "b122de9d-24eb-4bdc-adc7-4d0a68490740"; | ||
| export const AI_INGESTION_ENDPOINT = "https://westus2-2.in.applicationinsights.azure.com"; | ||
|
|
||
| export function resolveEnvironment(hostname: string): string { | ||
| if (hostname === "decipher.ms" || hostname === "www.decipher.ms") return "production"; | ||
| const previewMatch = hostname.match(/^([a-z0-9-]+)-decipher-ms\.[^.]+\.workers\.dev$/i); | ||
| if (previewMatch) return `preview:${previewMatch[1]}`; | ||
| if (hostname.endsWith(".workers.dev")) return "preview"; | ||
| return "development"; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ import { | |
| type LCPMetricWithAttribution, | ||
| type TTFBMetricWithAttribution, | ||
| } from "web-vitals/attribution"; | ||
| import { AI_IKEY, AI_INGESTION_ENDPOINT, resolveEnvironment } from "./telemetry-config"; | ||
|
|
||
| type VitalMetric = | ||
| | CLSMetricWithAttribution | ||
|
|
@@ -26,12 +27,6 @@ interface TelemetryConfig { | |
| environment: string; | ||
| } | ||
|
|
||
| declare global { | ||
| interface Window { | ||
| __telemetryConfig?: TelemetryConfig; | ||
| } | ||
| } | ||
|
|
||
| interface Envelope { | ||
| name: string; | ||
| time: string; | ||
|
|
@@ -255,8 +250,18 @@ let initialized = false; | |
| export function initTelemetry(): void { | ||
| if (initialized) return; | ||
| initialized = true; | ||
| config = window.__telemetryConfig ?? null; | ||
| if (!config) return; | ||
| // Identifiers are public constants; the environment is derived client-side so | ||
| // prerendered static pages need no per-request server-injected config. | ||
| const environment = resolveEnvironment(location.hostname); | ||
| // Stay silent on localhost: historically dev had no injected config and so | ||
| // never emitted; sending here would pollute the shared App Insights resource | ||
| // (and fire surprising cross-origin beacons during local work). | ||
| if (environment === "development") return; | ||
| config = { | ||
|
Comment on lines
250
to
+260
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch — confirmed during preview testing (a beacon actually fired from localhost). Fixed in c6ed36e: |
||
| iKey: AI_IKEY, | ||
| ingestionEndpoint: AI_INGESTION_ENDPOINT, | ||
| environment, | ||
| }; | ||
|
|
||
| onCLS(trackVital); | ||
| onINP(trackVital); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,12 @@ | ||
| import { defineMiddleware } from "astro:middleware"; | ||
| import { env } from "cloudflare:workers"; | ||
| import { Telemetry } from "@/lib/server/telemetry"; | ||
|
|
||
| // Server-side request telemetry, ported from the old worker entrypoint. Runs | ||
| // for every on-demand route (static assets are served by the platform and | ||
| // never reach here). The Telemetry instance is stashed on locals so endpoints | ||
| // and the Layout can attach spans / read the client config for the same span. | ||
| // for every on-demand route (prerendered pages and static assets are served by | ||
| // the platform and never reach here). The Telemetry instance is stashed on | ||
| // locals so endpoints can attach spans/exceptions to the same request span; the | ||
| // per-request client config injection is gone — the browser RUM client now | ||
| // self-configures from public constants (see lib/telemetry.ts). | ||
| export const onRequest = defineMiddleware(async (context, next) => { | ||
| const { request, url, locals } = context; | ||
| const ctx = locals.cfContext; | ||
|
|
@@ -14,7 +15,7 @@ export const onRequest = defineMiddleware(async (context, next) => { | |
| return next(); | ||
| } | ||
|
|
||
| const tel = new Telemetry(env, ctx, url.hostname); | ||
| const tel = new Telemetry(ctx, url.hostname); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated the comment in c6ed36e — |
||
| const requestId = tel.newSpanId(); | ||
| locals.telemetry = tel; | ||
| locals.requestId = requestId; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right — the old zod schema was
.trim().max()(validated the trimmed value) and we submit trimmed, so untrimmed length checks were a behavior change. Fixed in c6ed36e:validate()now checks trimmed values for all required/length rules (and reuses the trimmed string for the email regex). Verified:' Joe Smith 'now passes. 🤖 Claude Code