Skip to content

Commit 753b753

Browse files
committed
wip
1 parent 25025e5 commit 753b753

File tree

4 files changed

+265
-7
lines changed

4 files changed

+265
-7
lines changed

packages/viewer/src/analytics.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/**
2+
* Cloudflare Workers Analytics Engine integration utility
3+
*/
4+
5+
export interface AnalyticsEngineDataset {
6+
writeDataPoint(data: AnalyticsDataPoint): void
7+
}
8+
9+
export interface AnalyticsDataPoint {
10+
blobs?: (string | null)[]
11+
doubles?: number[]
12+
indexes?: (string | null)[]
13+
}
14+
15+
export interface WorkerMetrics {
16+
duration: number
17+
endpoint: string
18+
method: string
19+
status: number
20+
userAgent?: string
21+
country?: string
22+
timestamp: number
23+
}
24+
25+
/**
26+
* Helper class for sending metrics to Cloudflare Analytics Engine
27+
*/
28+
export class AnalyticsReporter {
29+
private analyticsEngine: AnalyticsEngineDataset | null = null
30+
31+
constructor(analyticsEngine?: AnalyticsEngineDataset) {
32+
this.analyticsEngine = analyticsEngine || null
33+
}
34+
35+
/**
36+
* Report worker execution metrics to Analytics Engine
37+
*/
38+
reportWorkerMetrics(metrics: WorkerMetrics): void {
39+
if (!this.analyticsEngine) {
40+
// In development or when Analytics Engine is not available
41+
console.log('Worker Metrics:', metrics)
42+
return
43+
}
44+
45+
try {
46+
this.analyticsEngine.writeDataPoint({
47+
// Indexed fields (can be queried and filtered)
48+
indexes: [
49+
metrics.endpoint, // index[1]: endpoint path
50+
metrics.method, // index[2]: HTTP method
51+
metrics.status.toString(), // index[3]: status code
52+
metrics.country || null, // index[4]: user country
53+
],
54+
// Blob fields (string data)
55+
blobs: [
56+
metrics.userAgent || null, // blob[1]: user agent
57+
],
58+
// Double fields (numeric data for aggregation)
59+
doubles: [
60+
metrics.duration, // double[1]: duration in milliseconds
61+
metrics.timestamp, // double[2]: timestamp
62+
],
63+
})
64+
} catch (error) {
65+
console.error('Failed to report analytics:', error)
66+
}
67+
}
68+
69+
/**
70+
* Report custom metrics to Analytics Engine
71+
*/
72+
reportCustomMetric(
73+
name: string,
74+
value: number,
75+
tags: Record<string, string | null> = {},
76+
): void {
77+
if (!this.analyticsEngine) {
78+
console.log('Custom Metric:', { name, value, tags })
79+
return
80+
}
81+
82+
try {
83+
const indexes = [name, null, null, null] // Start with metric name
84+
const tagKeys = Object.keys(tags).slice(0, 3) // Limit to 3 additional tags
85+
86+
tagKeys.forEach((key, index) => {
87+
indexes[index + 1] = tags[key]
88+
})
89+
90+
this.analyticsEngine.writeDataPoint({
91+
indexes: indexes as (string | null)[],
92+
doubles: [value, Date.now()],
93+
blobs: [null],
94+
})
95+
} catch (error) {
96+
console.error('Failed to report custom metric:', error)
97+
}
98+
}
99+
}
100+
101+
/**
102+
* Extract country from Cloudflare request headers
103+
*/
104+
export function getCountryFromRequest(request: Request): string | undefined {
105+
return request.headers.get('CF-IPCountry') || undefined
106+
}
107+
108+
/**
109+
* Create a performance timing wrapper for functions
110+
*/
111+
export function withTiming<T extends unknown[], R>(
112+
fn: (...args: T) => R | Promise<R>,
113+
onComplete: (duration: number) => void,
114+
): (...args: T) => R | Promise<R> {
115+
return (...args: T) => {
116+
const startTime = performance.now()
117+
118+
const handleComplete = (result: R) => {
119+
const duration = performance.now() - startTime
120+
onComplete(duration)
121+
return result
122+
}
123+
124+
try {
125+
const result = fn(...args)
126+
if (result instanceof Promise) {
127+
return result.then(handleComplete).catch(error => {
128+
const duration = performance.now() - startTime
129+
onComplete(duration)
130+
throw error
131+
})
132+
} else {
133+
return handleComplete(result)
134+
}
135+
} catch (error) {
136+
const duration = performance.now() - startTime
137+
onComplete(duration)
138+
throw error
139+
}
140+
}
141+
}

packages/viewer/src/app.d.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// See https://kit.svelte.dev/docs/types#app
2+
// for information about these interfaces
3+
declare global {
4+
namespace App {
5+
// interface Error {}
6+
// interface Locals {}
7+
// interface PageData {}
8+
// interface PageState {}
9+
interface Platform {
10+
env: {
11+
ANALYTICS_ENGINE?: import('./src/analytics.js').AnalyticsEngineDataset
12+
// Add other Cloudflare environment variables as needed
13+
[key: string]: unknown
14+
}
15+
context: {
16+
waitUntil(promise: Promise<any>): void
17+
}
18+
caches: CacheStorage & { default: Cache }
19+
}
20+
}
21+
}
22+
23+
export {}
Lines changed: 96 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,101 @@
11
import type { Handle } from '@sveltejs/kit'
2+
import {
3+
AnalyticsReporter,
4+
getCountryFromRequest,
5+
withTiming,
6+
type AnalyticsEngineDataset,
7+
} from './analytics.js'
28

39
// See https://kit.svelte.dev/docs/hooks#server-hooks
410
export const handle: Handle = async ({ event, resolve }) => {
5-
return resolve(event, {
6-
// We use the `etag` header from S3 to determine whether the bundle has
7-
// changed (and deduction needs to be re-run), so we need to preserve it.
8-
filterSerializedResponseHeaders(name, _value) {
9-
return name === 'etag'
10-
},
11-
})
11+
const startTime = performance.now()
12+
13+
// Initialize Analytics Engine if available (in Cloudflare Workers environment)
14+
const analyticsEngine = event.platform?.env?.ANALYTICS_ENGINE as
15+
| AnalyticsEngineDataset
16+
| undefined
17+
const analytics = new AnalyticsReporter(analyticsEngine)
18+
19+
// Extract request information for metrics
20+
const method = event.request.method
21+
const url = new URL(event.request.url)
22+
const endpoint = url.pathname
23+
const userAgent = event.request.headers.get('User-Agent') || undefined
24+
const country = getCountryFromRequest(event.request)
25+
26+
try {
27+
// Wrap the resolve function with timing
28+
const timedResolve = withTiming(
29+
(evt: typeof event, options: Parameters<typeof resolve>[1]) =>
30+
resolve(evt, options),
31+
duration => {
32+
// This will be called when resolve completes
33+
console.log(
34+
`Request ${method} ${endpoint} took ${duration.toFixed(2)}ms`,
35+
)
36+
},
37+
)
38+
39+
const response = await timedResolve(event, {
40+
// We use the `etag` header from S3 to determine whether the bundle has
41+
// changed (and deduction needs to be re-run), so we need to preserve it.
42+
filterSerializedResponseHeaders(name, _value) {
43+
return name === 'etag'
44+
},
45+
})
46+
47+
// Calculate total duration and report metrics
48+
const totalDuration = performance.now() - startTime
49+
50+
analytics.reportWorkerMetrics({
51+
duration: totalDuration,
52+
endpoint,
53+
method,
54+
status: response.status,
55+
userAgent,
56+
country,
57+
timestamp: Date.now(),
58+
})
59+
60+
// Report additional custom metrics based on response characteristics
61+
if (response.status >= 400) {
62+
analytics.reportCustomMetric('error_count', 1, {
63+
status: response.status.toString(),
64+
endpoint,
65+
method,
66+
})
67+
}
68+
69+
if (totalDuration > 1000) {
70+
// Slow requests > 1 second
71+
analytics.reportCustomMetric('slow_request_count', 1, {
72+
endpoint,
73+
method,
74+
duration_bucket: totalDuration > 5000 ? 'very_slow' : 'slow',
75+
})
76+
}
77+
78+
return response
79+
} catch (error) {
80+
const totalDuration = performance.now() - startTime
81+
82+
// Report error metrics
83+
analytics.reportWorkerMetrics({
84+
duration: totalDuration,
85+
endpoint,
86+
method,
87+
status: 500,
88+
userAgent,
89+
country,
90+
timestamp: Date.now(),
91+
})
92+
93+
analytics.reportCustomMetric('exception_count', 1, {
94+
endpoint,
95+
method,
96+
error_type: error instanceof Error ? error.name : 'unknown',
97+
})
98+
99+
throw error
100+
}
12101
}

packages/viewer/wrangler.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
compatibility_date = "2024-01-01"
2+
3+
[[analytics_engine_datasets]]
4+
binding = "ANALYTICS_ENGINE"
5+
dataset = "pi_base_worker_analytics"

0 commit comments

Comments
 (0)