-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
Copy pathsetup.ts
233 lines (208 loc) · 6.81 KB
/
setup.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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
import type {
ArgumentsHost,
CallHandler,
DynamicModule,
ExecutionContext,
NestInterceptor,
OnModuleInit,
} from '@nestjs/common';
import { Catch, Global, HttpException, Injectable, Logger, Module } from '@nestjs/common';
import type { HttpServer } from '@nestjs/common';
import { APP_INTERCEPTOR, BaseExceptionFilter } from '@nestjs/core';
import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
captureException,
getClient,
getDefaultIsolationScope,
getIsolationScope,
spanToJSON,
} from '@sentry/core';
import type { Span } from '@sentry/types';
import { logger } from '@sentry/utils';
import type { Observable } from 'rxjs';
import { isExpectedError } from './helpers';
/**
* Note: We cannot use @ syntax to add the decorators, so we add them directly below the classes as function wrappers.
*/
/**
* Interceptor to add Sentry tracing capabilities to Nest.js applications.
*/
class SentryTracingInterceptor implements NestInterceptor {
// used to exclude this class from being auto-instrumented
public readonly __SENTRY_INTERNAL__: boolean;
public constructor() {
this.__SENTRY_INTERNAL__ = true;
}
/**
* Intercepts HTTP requests to set the transaction name for Sentry tracing.
*/
public intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
if (getIsolationScope() === getDefaultIsolationScope()) {
logger.warn('Isolation scope is still the default isolation scope, skipping setting transactionName.');
return next.handle();
}
if (context.getType() === 'http') {
const req = context.switchToHttp().getRequest();
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (req.route) {
// eslint-disable-next-line @sentry-internal/sdk/no-optional-chaining,@typescript-eslint/no-unsafe-member-access
getIsolationScope().setTransactionName(`${req.method?.toUpperCase() || 'GET'} ${req.route.path}`);
}
}
return next.handle();
}
}
Injectable()(SentryTracingInterceptor);
export { SentryTracingInterceptor };
/**
* Global filter to handle exceptions and report them to Sentry.
*/
class SentryGlobalFilter extends BaseExceptionFilter {
public readonly __SENTRY_INTERNAL__: boolean;
public constructor(applicationRef?: HttpServer) {
super(applicationRef);
this.__SENTRY_INTERNAL__ = true;
}
/**
* Catches exceptions and reports them to Sentry unless they are expected errors.
*/
public catch(exception: unknown, host: ArgumentsHost): void {
if (isExpectedError(exception)) {
return super.catch(exception, host);
}
captureException(exception);
return super.catch(exception, host);
}
}
Catch()(SentryGlobalFilter);
export { SentryGlobalFilter };
/**
* Global filter to handle exceptions and report them to Sentry.
*
* The BaseExceptionFilter does not work well in GraphQL applications.
* By default, Nest GraphQL applications use the ExternalExceptionFilter, which just rethrows the error:
* https://github.com/nestjs/nest/blob/master/packages/core/exceptions/external-exception-filter.ts
*
* The ExternalExceptinFilter is not exported, so we reimplement this filter here.
*/
class SentryGlobalGraphQLFilter {
private static readonly _logger = new Logger('ExceptionsHandler');
public readonly __SENTRY_INTERNAL__: boolean;
public constructor() {
this.__SENTRY_INTERNAL__ = true;
}
/**
* Catches exceptions and reports them to Sentry unless they are HttpExceptions.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public catch(exception: unknown, host: ArgumentsHost): void {
// neither report nor log HttpExceptions
if (exception instanceof HttpException) {
throw exception;
}
if (exception instanceof Error) {
SentryGlobalGraphQLFilter._logger.error(exception.message, exception.stack);
}
captureException(exception);
throw exception;
}
}
Catch()(SentryGlobalGraphQLFilter);
export { SentryGlobalGraphQLFilter };
/**
* Global filter to handle exceptions and report them to Sentry.
*
* This filter is a generic filter that can handle both HTTP and GraphQL exceptions.
*/
class SentryGlobalGenericFilter extends SentryGlobalFilter {
public readonly __SENTRY_INTERNAL__: boolean;
private readonly _graphqlFilter: SentryGlobalGraphQLFilter;
public constructor(applicationRef?: HttpServer) {
super(applicationRef);
this.__SENTRY_INTERNAL__ = true;
this._graphqlFilter = new SentryGlobalGraphQLFilter();
}
/**
* Catches exceptions and forwards them to the according error filter.
*/
public catch(exception: unknown, host: ArgumentsHost): void {
if (host.getType<'graphql'>() === 'graphql') {
return this._graphqlFilter.catch(exception, host);
}
super.catch(exception, host);
}
}
Catch()(SentryGlobalGenericFilter);
export { SentryGlobalGenericFilter };
/**
* Service to set up Sentry performance tracing for Nest.js applications.
*/
class SentryService implements OnModuleInit {
public readonly __SENTRY_INTERNAL__: boolean;
public constructor() {
this.__SENTRY_INTERNAL__ = true;
}
/**
* Initializes the Sentry service and registers span attributes.
*/
public onModuleInit(): void {
// Sadly, NestInstrumentation has no requestHook, so we need to add the attributes here
// We register this hook in this method, because if we register it in the integration `setup`,
// it would always run even for users that are not even using Nest.js
const client = getClient();
if (client) {
client.on('spanStart', span => {
addNestSpanAttributes(span);
});
}
}
}
Injectable()(SentryService);
export { SentryService };
/**
* Set up a root module that can be injected in nest applications.
*/
class SentryModule {
/**
* Configures the module as the root module in a Nest.js application.
*/
public static forRoot(): DynamicModule {
return {
module: SentryModule,
providers: [
SentryService,
{
provide: APP_INTERCEPTOR,
useClass: SentryTracingInterceptor,
},
],
exports: [SentryService],
};
}
}
Global()(SentryModule);
Module({
providers: [
SentryService,
{
provide: APP_INTERCEPTOR,
useClass: SentryTracingInterceptor,
},
],
exports: [SentryService],
})(SentryModule);
export { SentryModule };
function addNestSpanAttributes(span: Span): void {
const attributes = spanToJSON(span).data || {};
// this is one of: app_creation, request_context, handler
const type = attributes['nestjs.type'];
// If this is already set, or we have no nest.js span, no need to process again...
if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] || !type) {
return;
}
span.setAttributes({
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.nestjs',
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: `${type}.nestjs`,
});
}