-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
Copy pathhandler.ts
138 lines (124 loc) · 4.92 KB
/
handler.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
import type {
ExportedHandler,
ExportedHandlerFetchHandler,
IncomingRequestCfProperties,
} from '@cloudflare/workers-types';
import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
captureException,
continueTrace,
flush,
setHttpStatus,
startSpan,
withIsolationScope,
} from '@sentry/core';
import type { Options, Scope, SpanAttributes } from '@sentry/types';
import { stripUrlQueryAndFragment, winterCGRequestToRequestData } from '@sentry/utils';
import { setAsyncLocalStorageAsyncContextStrategy } from './async';
import { init } from './sdk';
/**
* Extract environment generic from exported handler.
*/
type ExtractEnv<P> = P extends ExportedHandler<infer Env> ? Env : never;
/**
* Wrapper for Cloudflare handlers.
*
* Initializes the SDK and wraps the handler with Sentry instrumentation.
*
* Automatically instruments the `fetch` method of the handler.
*
* @param optionsCallback Function that returns the options for the SDK initialization.
* @param handler {ExportedHandler} The handler to wrap.
* @returns The wrapped handler.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function withSentry<E extends ExportedHandler<any>>(
optionsCallback: (env: ExtractEnv<E>) => Options,
handler: E,
): E {
setAsyncLocalStorageAsyncContextStrategy();
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
if ('fetch' in handler && typeof handler.fetch === 'function' && !(handler.fetch as any).__SENTRY_INSTRUMENTED__) {
handler.fetch = new Proxy(handler.fetch, {
apply(target, thisArg, args: Parameters<ExportedHandlerFetchHandler<ExtractEnv<E>>>) {
const [request, env, context] = args;
return withIsolationScope(isolationScope => {
const options = optionsCallback(env);
const client = init(options);
isolationScope.setClient(client);
const attributes: SpanAttributes = {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.cloudflare-worker',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server',
['http.request.method']: request.method,
['url.full']: request.url,
};
const contentLength = request.headers.get('content-length');
if (contentLength) {
attributes['http.request.body.size'] = parseInt(contentLength, 10);
}
let pathname = '';
try {
const url = new URL(request.url);
pathname = url.pathname;
attributes['server.address'] = url.hostname;
attributes['url.scheme'] = url.protocol.replace(':', '');
} catch {
// skip
}
addRequest(isolationScope, request);
addCloudResourceContext(isolationScope);
if (request.cf) {
addCultureContext(isolationScope, request.cf);
attributes['network.protocol.name'] = request.cf.httpProtocol;
}
const routeName = `${request.method} ${pathname ? stripUrlQueryAndFragment(pathname) : '/'}`;
return continueTrace(
{ sentryTrace: request.headers.get('sentry-trace') || '', baggage: request.headers.get('baggage') },
() => {
// Note: This span will not have a duration unless I/O happens in the handler. This is
// because of how the cloudflare workers runtime works.
// See: https://developers.cloudflare.com/workers/runtime-apis/performance/
return startSpan(
{
name: routeName,
attributes,
},
async span => {
try {
const res = await (target.apply(thisArg, args) as ReturnType<typeof target>);
setHttpStatus(span, res.status);
return res;
} catch (e) {
captureException(e, { mechanism: { handled: false, type: 'cloudflare' } });
throw e;
} finally {
context.waitUntil(flush(2000));
}
},
);
},
);
});
},
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
(handler.fetch as any).__SENTRY_INSTRUMENTED__ = true;
}
return handler;
}
function addCloudResourceContext(isolationScope: Scope): void {
isolationScope.setContext('cloud_resource', {
'cloud.provider': 'cloudflare',
});
}
function addCultureContext(isolationScope: Scope, cf: IncomingRequestCfProperties): void {
isolationScope.setContext('culture', {
timezone: cf.timezone,
});
}
function addRequest(isolationScope: Scope, request: Request): void {
isolationScope.setSDKProcessingMetadata({ request: winterCGRequestToRequestData(request) });
}