@sentry/ember 10.53.0 → 10.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/addon/index.ts +5 -0
- package/addon/instance-initializers/sentry-performance.ts +4 -495
- package/addon/utils/browserTracingIntegration.ts +87 -0
- package/addon/utils/instrumentEmberAppInstanceForPerformance.ts +175 -0
- package/addon/utils/instrumentEmberGlobals.ts +265 -0
- package/addon/utils/performance.ts +80 -0
- package/index.d.ts +4 -0
- package/instance-initializers/sentry-performance.d.ts +1 -6
- package/package.json +3 -3
- package/utils/browserTracingIntegration.d.ts +14 -0
- package/utils/instrumentEmberAppInstanceForPerformance.d.ts +10 -0
- package/utils/instrumentEmberGlobals.d.ts +9 -0
- package/utils/performance.d.ts +9 -0
package/addon/index.ts
CHANGED
|
@@ -119,3 +119,8 @@ export const instrumentRoutePerformance = <T extends RouteConstructor>(BaseRoute
|
|
|
119
119
|
};
|
|
120
120
|
|
|
121
121
|
export * from '@sentry/browser';
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Ember-specific browser tracing integration
|
|
125
|
+
*/
|
|
126
|
+
export { browserTracingIntegration } from './utils/browserTracingIntegration';
|
|
@@ -1,40 +1,5 @@
|
|
|
1
|
-
/* eslint-disable max-lines */
|
|
2
1
|
import type ApplicationInstance from '@ember/application/instance';
|
|
3
|
-
import {
|
|
4
|
-
import type Transition from '@ember/routing/-private/transition';
|
|
5
|
-
import type RouterService from '@ember/routing/router-service';
|
|
6
|
-
import { _backburner, run, scheduleOnce } from '@ember/runloop';
|
|
7
|
-
import type { EmberRunQueues } from '@ember/runloop/-private/types';
|
|
8
|
-
import { getOwnConfig, isTesting, macroCondition } from '@embroider/macros';
|
|
9
|
-
import type {
|
|
10
|
-
BrowserClient,
|
|
11
|
-
startBrowserTracingNavigationSpan as startBrowserTracingNavigationSpanType,
|
|
12
|
-
startBrowserTracingPageLoadSpan as startBrowserTracingPageLoadSpanType,
|
|
13
|
-
} from '@sentry/browser';
|
|
14
|
-
import {
|
|
15
|
-
getActiveSpan,
|
|
16
|
-
getClient,
|
|
17
|
-
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
|
|
18
|
-
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
|
|
19
|
-
startInactiveSpan,
|
|
20
|
-
} from '@sentry/browser';
|
|
21
|
-
import type { Span } from '@sentry/core';
|
|
22
|
-
import { addIntegration, browserPerformanceTimeOrigin, GLOBAL_OBJ, timestampInSeconds } from '@sentry/core';
|
|
23
|
-
import type { ExtendedBackburner } from '@sentry/ember/runloop';
|
|
24
|
-
import type { EmberRouterMain, EmberSentryConfig, GlobalConfig, OwnConfig } from '../types';
|
|
25
|
-
|
|
26
|
-
function getSentryConfig(): EmberSentryConfig {
|
|
27
|
-
const _global = GLOBAL_OBJ as typeof GLOBAL_OBJ & GlobalConfig;
|
|
28
|
-
_global.__sentryEmberConfig = _global.__sentryEmberConfig ?? {};
|
|
29
|
-
const environmentConfig = getOwnConfig<OwnConfig>().sentryConfig;
|
|
30
|
-
if (!environmentConfig.sentry) {
|
|
31
|
-
environmentConfig.sentry = {
|
|
32
|
-
browserTracingOptions: {},
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
Object.assign(environmentConfig.sentry, _global.__sentryEmberConfig);
|
|
36
|
-
return environmentConfig;
|
|
37
|
-
}
|
|
2
|
+
import { instrumentForPerformance, getSentryConfig } from '../utils/performance';
|
|
38
3
|
|
|
39
4
|
export function initialize(appInstance: ApplicationInstance): void {
|
|
40
5
|
// Disable in fastboot - we only want to run Sentry client-side
|
|
@@ -47,468 +12,12 @@ export function initialize(appInstance: ApplicationInstance): void {
|
|
|
47
12
|
if (config['disablePerformance']) {
|
|
48
13
|
return;
|
|
49
14
|
}
|
|
50
|
-
const performancePromise = instrumentForPerformance(appInstance);
|
|
51
|
-
if (macroCondition(isTesting())) {
|
|
52
|
-
(window as typeof window & { _sentryPerformanceLoad?: Promise<void> })._sentryPerformanceLoad = performancePromise;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function getBackburner(): Pick<ExtendedBackburner, 'on' | 'off'> {
|
|
57
|
-
if (_backburner) {
|
|
58
|
-
return _backburner as unknown as Pick<ExtendedBackburner, 'on' | 'off'>;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if ((run as unknown as { backburner?: Pick<ExtendedBackburner, 'on' | 'off'> }).backburner) {
|
|
62
|
-
return (run as unknown as { backburner: Pick<ExtendedBackburner, 'on' | 'off'> }).backburner;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
on() {
|
|
67
|
-
// noop
|
|
68
|
-
},
|
|
69
|
-
off() {
|
|
70
|
-
// noop
|
|
71
|
-
},
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function getTransitionInformation(
|
|
76
|
-
transition: Transition | undefined,
|
|
77
|
-
router: RouterService,
|
|
78
|
-
): { fromRoute?: string; toRoute?: string } {
|
|
79
|
-
const fromRoute = transition?.from?.name;
|
|
80
|
-
const toRoute = transition?.to?.name || router.currentRouteName;
|
|
81
|
-
return {
|
|
82
|
-
fromRoute,
|
|
83
|
-
toRoute,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Only exported for testing
|
|
88
|
-
export function _getLocationURL(location: EmberRouterMain['location']): string {
|
|
89
|
-
if (!location?.getURL || !location?.formatURL) {
|
|
90
|
-
return '';
|
|
91
|
-
}
|
|
92
|
-
const url = location.formatURL(location.getURL());
|
|
93
|
-
|
|
94
|
-
// `implementation` is optional in Ember's predefined location types, so we also check if the URL starts with '#'.
|
|
95
|
-
if (location.implementation === 'hash' || url.startsWith('#')) {
|
|
96
|
-
return `${location.rootURL}${url}`;
|
|
97
|
-
}
|
|
98
|
-
return url;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function _instrumentEmberRouter(
|
|
102
|
-
routerService: RouterService,
|
|
103
|
-
routerMain: EmberRouterMain,
|
|
104
|
-
config: EmberSentryConfig,
|
|
105
|
-
startBrowserTracingPageLoadSpan: typeof startBrowserTracingPageLoadSpanType,
|
|
106
|
-
startBrowserTracingNavigationSpan: typeof startBrowserTracingNavigationSpanType,
|
|
107
|
-
): void {
|
|
108
|
-
const { disableRunloopPerformance } = config;
|
|
109
|
-
const location = routerMain.location;
|
|
110
|
-
let activeRootSpan: Span | undefined;
|
|
111
|
-
let transitionSpan: Span | undefined;
|
|
112
|
-
|
|
113
|
-
// Maintaining backwards compatibility with config.browserTracingOptions, but passing it with Sentry options is preferred.
|
|
114
|
-
const browserTracingOptions = config.browserTracingOptions || config.sentry.browserTracingOptions || {};
|
|
115
|
-
const url = _getLocationURL(location);
|
|
116
|
-
|
|
117
|
-
const client = getClient<BrowserClient>();
|
|
118
|
-
|
|
119
|
-
if (!client) {
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (url && browserTracingOptions.instrumentPageLoad !== false) {
|
|
124
|
-
const routeInfo = routerService.recognize(url);
|
|
125
|
-
activeRootSpan = startBrowserTracingPageLoadSpan(client, {
|
|
126
|
-
name: `route:${routeInfo.name}`,
|
|
127
|
-
attributes: {
|
|
128
|
-
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
|
|
129
|
-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.ember',
|
|
130
|
-
url,
|
|
131
|
-
toRoute: routeInfo.name,
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const finishActiveTransaction = (_: unknown, nextInstance: unknown): void => {
|
|
137
|
-
if (nextInstance) {
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
activeRootSpan?.end();
|
|
141
|
-
getBackburner().off('end', finishActiveTransaction);
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
if (browserTracingOptions.instrumentNavigation === false) {
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
routerService.on('routeWillChange', (transition: Transition) => {
|
|
149
|
-
const { fromRoute, toRoute } = getTransitionInformation(transition, routerService);
|
|
150
|
-
|
|
151
|
-
// We want to ignore loading && error routes
|
|
152
|
-
if (transitionIsIntermediate(transition)) {
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
activeRootSpan?.end();
|
|
157
|
-
|
|
158
|
-
activeRootSpan = startBrowserTracingNavigationSpan(client, {
|
|
159
|
-
name: `route:${toRoute}`,
|
|
160
|
-
attributes: {
|
|
161
|
-
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
|
|
162
|
-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.ember',
|
|
163
|
-
fromRoute,
|
|
164
|
-
toRoute,
|
|
165
|
-
},
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
transitionSpan = startInactiveSpan({
|
|
169
|
-
attributes: {
|
|
170
|
-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember',
|
|
171
|
-
},
|
|
172
|
-
op: 'ui.ember.transition',
|
|
173
|
-
name: `route:${fromRoute} -> route:${toRoute}`,
|
|
174
|
-
onlyIfParent: true,
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
routerService.on('routeDidChange', transition => {
|
|
179
|
-
if (!transitionSpan || !activeRootSpan || transitionIsIntermediate(transition)) {
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
transitionSpan.end();
|
|
183
|
-
|
|
184
|
-
if (disableRunloopPerformance) {
|
|
185
|
-
activeRootSpan.end();
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
getBackburner().on('end', finishActiveTransaction);
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function _instrumentEmberRunloop(config: EmberSentryConfig): void {
|
|
194
|
-
const { disableRunloopPerformance, minimumRunloopQueueDuration } = config;
|
|
195
|
-
if (disableRunloopPerformance) {
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
let currentQueueStart: number | undefined;
|
|
200
|
-
let currentQueueSpan: Span | undefined;
|
|
201
|
-
const instrumentedEmberQueues = [
|
|
202
|
-
'actions',
|
|
203
|
-
'routerTransitions',
|
|
204
|
-
'render',
|
|
205
|
-
'afterRender',
|
|
206
|
-
'destroy',
|
|
207
|
-
] as EmberRunQueues[];
|
|
208
|
-
|
|
209
|
-
getBackburner().on('begin', (_: unknown, previousInstance: unknown) => {
|
|
210
|
-
if (previousInstance) {
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
const activeSpan = getActiveSpan();
|
|
214
|
-
if (!activeSpan) {
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
if (currentQueueSpan) {
|
|
218
|
-
currentQueueSpan.end();
|
|
219
|
-
}
|
|
220
|
-
currentQueueStart = timestampInSeconds();
|
|
221
|
-
|
|
222
|
-
const processQueue = (queue: EmberRunQueues): void => {
|
|
223
|
-
// Process this queue using the end of the previous queue.
|
|
224
|
-
if (currentQueueStart) {
|
|
225
|
-
const now = timestampInSeconds();
|
|
226
|
-
const minQueueDuration = minimumRunloopQueueDuration ?? 5;
|
|
227
|
-
|
|
228
|
-
if ((now - currentQueueStart) * 1000 >= minQueueDuration) {
|
|
229
|
-
startInactiveSpan({
|
|
230
|
-
attributes: {
|
|
231
|
-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember',
|
|
232
|
-
},
|
|
233
|
-
name: 'runloop',
|
|
234
|
-
op: `ui.ember.runloop.${queue}`,
|
|
235
|
-
startTime: currentQueueStart,
|
|
236
|
-
onlyIfParent: true,
|
|
237
|
-
})?.end(now);
|
|
238
|
-
}
|
|
239
|
-
currentQueueStart = undefined;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Setup for next queue
|
|
243
15
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
currentQueueStart = timestampInSeconds();
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
instrumentedEmberQueues.forEach(queue => {
|
|
252
|
-
scheduleOnce(queue, null, processQueue, queue);
|
|
253
|
-
});
|
|
254
|
-
});
|
|
255
|
-
getBackburner().on('end', (_: unknown, nextInstance: unknown) => {
|
|
256
|
-
if (nextInstance) {
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
if (currentQueueSpan) {
|
|
260
|
-
currentQueueSpan.end();
|
|
261
|
-
currentQueueSpan = undefined;
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
type Payload = {
|
|
267
|
-
containerKey: string;
|
|
268
|
-
initialRender: true;
|
|
269
|
-
object: string;
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
type RenderEntry = {
|
|
273
|
-
payload: Payload;
|
|
274
|
-
now: number;
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
interface RenderEntries {
|
|
278
|
-
[name: string]: RenderEntry;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function processComponentRenderBefore(payload: Payload, beforeEntries: RenderEntries): void {
|
|
282
|
-
const info = {
|
|
283
|
-
payload,
|
|
284
|
-
now: timestampInSeconds(),
|
|
285
|
-
};
|
|
286
|
-
beforeEntries[payload.object] = info;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function processComponentRenderAfter(
|
|
290
|
-
payload: Payload,
|
|
291
|
-
beforeEntries: RenderEntries,
|
|
292
|
-
op: string,
|
|
293
|
-
minComponentDuration: number,
|
|
294
|
-
): void {
|
|
295
|
-
const begin = beforeEntries[payload.object];
|
|
296
|
-
|
|
297
|
-
if (!begin) {
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const now = timestampInSeconds();
|
|
302
|
-
const componentRenderDuration = now - begin.now;
|
|
303
|
-
|
|
304
|
-
if (componentRenderDuration * 1000 >= minComponentDuration) {
|
|
305
|
-
startInactiveSpan({
|
|
306
|
-
name: payload.containerKey || payload.object,
|
|
307
|
-
op,
|
|
308
|
-
startTime: begin.now,
|
|
309
|
-
attributes: {
|
|
310
|
-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember',
|
|
311
|
-
},
|
|
312
|
-
onlyIfParent: true,
|
|
313
|
-
})?.end(now);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function _instrumentComponents(config: EmberSentryConfig): void {
|
|
318
|
-
const { disableInstrumentComponents, minimumComponentRenderDuration, enableComponentDefinitions } = config;
|
|
319
|
-
if (disableInstrumentComponents) {
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const minComponentDuration = minimumComponentRenderDuration ?? 2;
|
|
324
|
-
|
|
325
|
-
const beforeEntries = {} as RenderEntries;
|
|
326
|
-
const beforeComponentDefinitionEntries = {} as RenderEntries;
|
|
327
|
-
|
|
328
|
-
function _subscribeToRenderEvents(): void {
|
|
329
|
-
subscribe('render.component', {
|
|
330
|
-
before(_name: string, _timestamp: number, payload: Payload) {
|
|
331
|
-
processComponentRenderBefore(payload, beforeEntries);
|
|
332
|
-
},
|
|
333
|
-
|
|
334
|
-
after(_name: string, _timestamp: number, payload: Payload, _beganIndex: number) {
|
|
335
|
-
processComponentRenderAfter(payload, beforeEntries, 'ui.ember.component.render', minComponentDuration);
|
|
336
|
-
},
|
|
337
|
-
});
|
|
338
|
-
if (enableComponentDefinitions) {
|
|
339
|
-
subscribe('render.getComponentDefinition', {
|
|
340
|
-
before(_name: string, _timestamp: number, payload: Payload) {
|
|
341
|
-
processComponentRenderBefore(payload, beforeComponentDefinitionEntries);
|
|
342
|
-
},
|
|
343
|
-
|
|
344
|
-
after(_name: string, _timestamp: number, payload: Payload, _beganIndex: number) {
|
|
345
|
-
processComponentRenderAfter(payload, beforeComponentDefinitionEntries, 'ui.ember.component.definition', 0);
|
|
346
|
-
},
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
_subscribeToRenderEvents();
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function _instrumentInitialLoad(config: EmberSentryConfig): void {
|
|
354
|
-
const startName = '@sentry/ember:initial-load-start';
|
|
355
|
-
const endName = '@sentry/ember:initial-load-end';
|
|
356
|
-
|
|
357
|
-
const { HAS_PERFORMANCE, HAS_PERFORMANCE_TIMING } = _hasPerformanceSupport();
|
|
358
|
-
|
|
359
|
-
if (!HAS_PERFORMANCE) {
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const { performance } = window;
|
|
364
|
-
|
|
365
|
-
if (config.disableInitialLoadInstrumentation) {
|
|
366
|
-
performance.clearMarks(startName);
|
|
367
|
-
performance.clearMarks(endName);
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
const origin = browserPerformanceTimeOrigin();
|
|
372
|
-
// Split performance check in two so clearMarks still happens even if timeOrigin isn't available.
|
|
373
|
-
if (!HAS_PERFORMANCE_TIMING || origin === undefined) {
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
const measureName = '@sentry/ember:initial-load';
|
|
377
|
-
|
|
378
|
-
const startMarkExists = performance.getEntriesByName(startName).length > 0;
|
|
379
|
-
const endMarkExists = performance.getEntriesByName(endName).length > 0;
|
|
380
|
-
if (!startMarkExists || !endMarkExists) {
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
performance.measure(measureName, startName, endName);
|
|
385
|
-
const measures = performance.getEntriesByName(measureName);
|
|
386
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
387
|
-
const measure = measures[0]!;
|
|
388
|
-
|
|
389
|
-
const startTime = (measure.startTime + origin) / 1000;
|
|
390
|
-
const endTime = startTime + measure.duration / 1000;
|
|
391
|
-
|
|
392
|
-
startInactiveSpan({
|
|
393
|
-
op: 'ui.ember.init',
|
|
394
|
-
name: 'init',
|
|
395
|
-
attributes: {
|
|
396
|
-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember',
|
|
397
|
-
},
|
|
398
|
-
startTime,
|
|
399
|
-
onlyIfParent: true,
|
|
400
|
-
})?.end(endTime);
|
|
401
|
-
performance.clearMarks(startName);
|
|
402
|
-
performance.clearMarks(endName);
|
|
403
|
-
|
|
404
|
-
performance.clearMeasures(measureName);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
function _hasPerformanceSupport(): { HAS_PERFORMANCE: boolean; HAS_PERFORMANCE_TIMING: boolean } {
|
|
408
|
-
// TS says that all of these methods are always available, but some of them may not be supported in older browsers
|
|
409
|
-
// So we "pretend" they are all optional in order to be able to check this properly without TS complaining
|
|
410
|
-
const _performance = window.performance as {
|
|
411
|
-
clearMarks?: Performance['clearMarks'];
|
|
412
|
-
clearMeasures?: Performance['clearMeasures'];
|
|
413
|
-
measure?: Performance['measure'];
|
|
414
|
-
getEntriesByName?: Performance['getEntriesByName'];
|
|
415
|
-
};
|
|
416
|
-
const HAS_PERFORMANCE = Boolean(_performance?.clearMarks && _performance.clearMeasures);
|
|
417
|
-
const HAS_PERFORMANCE_TIMING = Boolean(
|
|
418
|
-
_performance.measure && _performance.getEntriesByName && browserPerformanceTimeOrigin !== undefined,
|
|
419
|
-
);
|
|
420
|
-
|
|
421
|
-
return {
|
|
422
|
-
HAS_PERFORMANCE,
|
|
423
|
-
HAS_PERFORMANCE_TIMING,
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
export async function instrumentForPerformance(appInstance: ApplicationInstance): Promise<void> {
|
|
428
|
-
const config = getSentryConfig();
|
|
429
|
-
// Maintaining backwards compatibility with config.browserTracingOptions, but passing it with Sentry options is preferred.
|
|
430
|
-
const browserTracingOptions = config.browserTracingOptions || config.sentry.browserTracingOptions || {};
|
|
431
|
-
|
|
432
|
-
const { browserTracingIntegration, startBrowserTracingNavigationSpan, startBrowserTracingPageLoadSpan } =
|
|
433
|
-
await import('@sentry/browser');
|
|
434
|
-
|
|
435
|
-
const idleTimeout = config.transitionTimeout || 5000;
|
|
436
|
-
|
|
437
|
-
const browserTracing = browserTracingIntegration({
|
|
438
|
-
idleTimeout,
|
|
439
|
-
...browserTracingOptions,
|
|
440
|
-
instrumentNavigation: false,
|
|
441
|
-
instrumentPageLoad: false,
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
const client = getClient<BrowserClient>();
|
|
445
|
-
const isAlreadyInitialized = macroCondition(isTesting()) ? !!client?.getIntegrationByName('BrowserTracing') : false;
|
|
446
|
-
addIntegration(browserTracing);
|
|
447
|
-
|
|
448
|
-
// We _always_ call this, as it triggers the page load & navigation spans
|
|
449
|
-
_instrumentNavigation(appInstance, config, startBrowserTracingPageLoadSpan, startBrowserTracingNavigationSpan);
|
|
450
|
-
|
|
451
|
-
// Skip instrumenting the stuff below again in tests, as these are not reset between tests
|
|
452
|
-
if (isAlreadyInitialized) {
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
_instrumentEmberRunloop(config);
|
|
457
|
-
_instrumentComponents(config);
|
|
458
|
-
_instrumentInitialLoad(config);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
function _instrumentNavigation(
|
|
462
|
-
appInstance: ApplicationInstance,
|
|
463
|
-
config: EmberSentryConfig,
|
|
464
|
-
startBrowserTracingPageLoadSpan: typeof startBrowserTracingPageLoadSpanType,
|
|
465
|
-
startBrowserTracingNavigationSpan: typeof startBrowserTracingNavigationSpanType,
|
|
466
|
-
): void {
|
|
467
|
-
// eslint-disable-next-line ember/no-private-routing-service
|
|
468
|
-
const routerMain = appInstance.lookup('router:main') as EmberRouterMain;
|
|
469
|
-
let routerService = appInstance.lookup('service:router') as RouterService & {
|
|
470
|
-
externalRouter?: RouterService;
|
|
471
|
-
_hasMountedSentryPerformanceRouting?: boolean;
|
|
472
|
-
};
|
|
473
|
-
|
|
474
|
-
if (routerService.externalRouter) {
|
|
475
|
-
// Using ember-engines-router-service in an engine.
|
|
476
|
-
routerService = routerService.externalRouter;
|
|
477
|
-
}
|
|
478
|
-
if (routerService._hasMountedSentryPerformanceRouting) {
|
|
479
|
-
// Routing listens to route changes on the main router, and should not be initialized multiple times per page.
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
if (!routerService.recognize) {
|
|
483
|
-
// Router is missing critical functionality to limit cardinality of the transaction names.
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
routerService._hasMountedSentryPerformanceRouting = true;
|
|
488
|
-
_instrumentEmberRouter(
|
|
489
|
-
routerService,
|
|
490
|
-
routerMain,
|
|
491
|
-
config,
|
|
492
|
-
startBrowserTracingPageLoadSpan,
|
|
493
|
-
startBrowserTracingNavigationSpan,
|
|
494
|
-
);
|
|
16
|
+
// Run this in the next tick to ensure the ember router etc. is properly initialized
|
|
17
|
+
instrumentForPerformance(appInstance);
|
|
495
18
|
}
|
|
496
19
|
|
|
497
20
|
export default {
|
|
498
21
|
initialize,
|
|
22
|
+
name: 'sentry-performance',
|
|
499
23
|
};
|
|
500
|
-
|
|
501
|
-
function transitionIsIntermediate(transition: Transition): boolean {
|
|
502
|
-
// We want to use ignore, as this may actually be defined on new versions
|
|
503
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
504
|
-
// @ts-ignore This actually exists on newer versions
|
|
505
|
-
const isIntermediate: boolean | undefined = transition.isIntermediate;
|
|
506
|
-
|
|
507
|
-
if (typeof isIntermediate === 'boolean') {
|
|
508
|
-
return isIntermediate;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// For versions without this, we look if the route is a `.loading` or `.error` route
|
|
512
|
-
// This is not perfect and may false-positive in some cases, but it's the best we can do
|
|
513
|
-
return transition.to?.localName === 'loading' || transition.to?.localName === 'error';
|
|
514
|
-
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {
|
|
2
|
+
browserTracingIntegration as originalBrowserTracingIntegration,
|
|
3
|
+
startBrowserTracingNavigationSpan,
|
|
4
|
+
startBrowserTracingPageLoadSpan,
|
|
5
|
+
} from '@sentry/browser';
|
|
6
|
+
import { consoleSandbox, type Integration } from '@sentry/core';
|
|
7
|
+
import type ApplicationInstance from '@ember/application/instance';
|
|
8
|
+
import { instrumentEmberAppInstanceForPerformance } from './instrumentEmberAppInstanceForPerformance';
|
|
9
|
+
import { instrumentGlobalsForPerformance } from './instrumentEmberGlobals';
|
|
10
|
+
import { isTesting, macroCondition } from '@embroider/macros';
|
|
11
|
+
|
|
12
|
+
type EmberBrowserTracingIntegrationOptions = Parameters<typeof originalBrowserTracingIntegration>[0] & {
|
|
13
|
+
// TODO(v11): make this required
|
|
14
|
+
appInstance?: ApplicationInstance;
|
|
15
|
+
disableRunloopPerformance?: boolean;
|
|
16
|
+
minimumRunloopQueueDuration?: number;
|
|
17
|
+
disableInstrumentComponents?: boolean;
|
|
18
|
+
minimumComponentRenderDuration?: number;
|
|
19
|
+
enableComponentDefinitions?: boolean;
|
|
20
|
+
disableInitialLoadInstrumentation?: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
let _initialized = false;
|
|
24
|
+
|
|
25
|
+
export function browserTracingIntegration(options: EmberBrowserTracingIntegrationOptions): Integration {
|
|
26
|
+
const { appInstance } = options;
|
|
27
|
+
|
|
28
|
+
const instrumentNavigation = options.instrumentNavigation ?? true;
|
|
29
|
+
const instrumentPageLoad = options.instrumentPageLoad ?? true;
|
|
30
|
+
|
|
31
|
+
const integration = originalBrowserTracingIntegration({
|
|
32
|
+
...options,
|
|
33
|
+
instrumentNavigation: false,
|
|
34
|
+
instrumentPageLoad: false,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const appInstancePerformanceConfig = {
|
|
38
|
+
disableRunloopPerformance: options.disableRunloopPerformance ?? false,
|
|
39
|
+
instrumentPageLoad,
|
|
40
|
+
instrumentNavigation,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const globalsPerformanceConfig = {
|
|
44
|
+
disableRunloopPerformance: options.disableRunloopPerformance ?? false,
|
|
45
|
+
minimumRunloopQueueDuration: options.minimumRunloopQueueDuration,
|
|
46
|
+
disableInstrumentComponents: options.disableInstrumentComponents ?? false,
|
|
47
|
+
minimumComponentRenderDuration: options.minimumComponentRenderDuration,
|
|
48
|
+
enableComponentDefinitions: options.enableComponentDefinitions ?? false,
|
|
49
|
+
disableInitialLoadInstrumentation: options.disableInitialLoadInstrumentation ?? false,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
...integration,
|
|
54
|
+
afterAllSetup(client) {
|
|
55
|
+
integration.afterAllSetup(client);
|
|
56
|
+
|
|
57
|
+
// Run this in the next tick to ensure the ember router etc. is properly initialized
|
|
58
|
+
|
|
59
|
+
setTimeout(() => {
|
|
60
|
+
if (appInstance) {
|
|
61
|
+
instrumentEmberAppInstanceForPerformance(
|
|
62
|
+
client,
|
|
63
|
+
appInstance,
|
|
64
|
+
appInstancePerformanceConfig,
|
|
65
|
+
startBrowserTracingPageLoadSpan,
|
|
66
|
+
startBrowserTracingNavigationSpan,
|
|
67
|
+
);
|
|
68
|
+
} else {
|
|
69
|
+
consoleSandbox(() => {
|
|
70
|
+
// eslint-disable-next-line no-console
|
|
71
|
+
console.warn('Skipping router instrumentation because appInstance is not provided.');
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// We only want to run this once in tests!
|
|
76
|
+
if (macroCondition(isTesting())) {
|
|
77
|
+
if (_initialized) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
instrumentGlobalsForPerformance(globalsPerformanceConfig);
|
|
83
|
+
_initialized = true;
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type ApplicationInstance from '@ember/application/instance';
|
|
2
|
+
import type Transition from '@ember/routing/-private/transition';
|
|
3
|
+
import type RouterService from '@ember/routing/router-service';
|
|
4
|
+
import type {
|
|
5
|
+
startBrowserTracingNavigationSpan as startBrowserTracingNavigationSpanType,
|
|
6
|
+
startBrowserTracingPageLoadSpan as startBrowserTracingPageLoadSpanType,
|
|
7
|
+
} from '@sentry/browser';
|
|
8
|
+
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, startInactiveSpan } from '@sentry/browser';
|
|
9
|
+
import type { Client, Span } from '@sentry/core';
|
|
10
|
+
import type { EmberRouterMain } from '../types';
|
|
11
|
+
import { getBackburner } from './performance';
|
|
12
|
+
|
|
13
|
+
export function instrumentEmberAppInstanceForPerformance(
|
|
14
|
+
client: Client,
|
|
15
|
+
appInstance: ApplicationInstance,
|
|
16
|
+
config: { disableRunloopPerformance?: boolean; instrumentPageLoad?: boolean; instrumentNavigation?: boolean },
|
|
17
|
+
startBrowserTracingPageLoadSpan: typeof startBrowserTracingPageLoadSpanType,
|
|
18
|
+
startBrowserTracingNavigationSpan: typeof startBrowserTracingNavigationSpanType,
|
|
19
|
+
): void {
|
|
20
|
+
// eslint-disable-next-line ember/no-private-routing-service
|
|
21
|
+
const routerMain = appInstance.lookup('router:main') as EmberRouterMain;
|
|
22
|
+
let routerService = appInstance.lookup('service:router') as RouterService & {
|
|
23
|
+
externalRouter?: RouterService;
|
|
24
|
+
_hasMountedSentryPerformanceRouting?: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
if (routerService.externalRouter) {
|
|
28
|
+
// Using ember-engines-router-service in an engine.
|
|
29
|
+
routerService = routerService.externalRouter;
|
|
30
|
+
}
|
|
31
|
+
if (routerService._hasMountedSentryPerformanceRouting) {
|
|
32
|
+
// Routing listens to route changes on the main router, and should not be initialized multiple times per page.
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (!routerService.recognize) {
|
|
36
|
+
// Router is missing critical functionality to limit cardinality of the transaction names.
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
routerService._hasMountedSentryPerformanceRouting = true;
|
|
41
|
+
_instrumentEmberRouter(
|
|
42
|
+
client,
|
|
43
|
+
routerService,
|
|
44
|
+
routerMain,
|
|
45
|
+
config,
|
|
46
|
+
startBrowserTracingPageLoadSpan,
|
|
47
|
+
startBrowserTracingNavigationSpan,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getTransitionInformation(
|
|
52
|
+
transition: Transition | undefined,
|
|
53
|
+
router: RouterService,
|
|
54
|
+
): { fromRoute?: string; toRoute?: string } {
|
|
55
|
+
const fromRoute = transition?.from?.name;
|
|
56
|
+
const toRoute = transition?.to?.name || router.currentRouteName;
|
|
57
|
+
return {
|
|
58
|
+
fromRoute,
|
|
59
|
+
toRoute,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Only exported for testing
|
|
64
|
+
export function _getLocationURL(location: EmberRouterMain['location']): string {
|
|
65
|
+
if (!location?.getURL || !location?.formatURL) {
|
|
66
|
+
return '';
|
|
67
|
+
}
|
|
68
|
+
const url = location.formatURL(location.getURL());
|
|
69
|
+
|
|
70
|
+
// `implementation` is optional in Ember's predefined location types, so we also check if the URL starts with '#'.
|
|
71
|
+
if (location.implementation === 'hash' || url.startsWith('#')) {
|
|
72
|
+
return `${location.rootURL}${url}`;
|
|
73
|
+
}
|
|
74
|
+
return url;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function _instrumentEmberRouter(
|
|
78
|
+
client: Client,
|
|
79
|
+
routerService: RouterService,
|
|
80
|
+
routerMain: EmberRouterMain,
|
|
81
|
+
config: { disableRunloopPerformance?: boolean; instrumentPageLoad?: boolean; instrumentNavigation?: boolean },
|
|
82
|
+
startBrowserTracingPageLoadSpan: typeof startBrowserTracingPageLoadSpanType,
|
|
83
|
+
startBrowserTracingNavigationSpan: typeof startBrowserTracingNavigationSpanType,
|
|
84
|
+
): void {
|
|
85
|
+
const { disableRunloopPerformance, instrumentPageLoad, instrumentNavigation } = config;
|
|
86
|
+
const location = routerMain.location;
|
|
87
|
+
let activeRootSpan: Span | undefined;
|
|
88
|
+
let transitionSpan: Span | undefined;
|
|
89
|
+
|
|
90
|
+
const url = _getLocationURL(location);
|
|
91
|
+
|
|
92
|
+
if (url && instrumentPageLoad !== false) {
|
|
93
|
+
const routeInfo = routerService.recognize(url);
|
|
94
|
+
activeRootSpan = startBrowserTracingPageLoadSpan(client, {
|
|
95
|
+
name: `route:${routeInfo.name}`,
|
|
96
|
+
attributes: {
|
|
97
|
+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
|
|
98
|
+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.ember',
|
|
99
|
+
url,
|
|
100
|
+
toRoute: routeInfo.name,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const finishActiveTransaction = (_: unknown, nextInstance: unknown): void => {
|
|
106
|
+
if (nextInstance) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
activeRootSpan?.end();
|
|
110
|
+
getBackburner().off('end', finishActiveTransaction);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (instrumentNavigation === false) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
routerService.on('routeWillChange', (transition: Transition) => {
|
|
118
|
+
const { fromRoute, toRoute } = getTransitionInformation(transition, routerService);
|
|
119
|
+
|
|
120
|
+
// We want to ignore loading && error routes
|
|
121
|
+
if (transitionIsIntermediate(transition)) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
activeRootSpan?.end();
|
|
126
|
+
|
|
127
|
+
activeRootSpan = startBrowserTracingNavigationSpan(client, {
|
|
128
|
+
name: `route:${toRoute}`,
|
|
129
|
+
attributes: {
|
|
130
|
+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
|
|
131
|
+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.ember',
|
|
132
|
+
fromRoute,
|
|
133
|
+
toRoute,
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
transitionSpan = startInactiveSpan({
|
|
138
|
+
attributes: {
|
|
139
|
+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember',
|
|
140
|
+
},
|
|
141
|
+
op: 'ui.ember.transition',
|
|
142
|
+
name: `route:${fromRoute} -> route:${toRoute}`,
|
|
143
|
+
onlyIfParent: true,
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
routerService.on('routeDidChange', transition => {
|
|
148
|
+
if (!transitionSpan || !activeRootSpan || transitionIsIntermediate(transition)) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
transitionSpan.end();
|
|
152
|
+
|
|
153
|
+
if (disableRunloopPerformance) {
|
|
154
|
+
activeRootSpan.end();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
getBackburner().on('end', finishActiveTransaction);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function transitionIsIntermediate(transition: Transition): boolean {
|
|
163
|
+
// We want to use ignore, as this may actually be defined on new versions
|
|
164
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
165
|
+
// @ts-ignore This actually exists on newer versions
|
|
166
|
+
const isIntermediate: boolean | undefined = transition.isIntermediate;
|
|
167
|
+
|
|
168
|
+
if (typeof isIntermediate === 'boolean') {
|
|
169
|
+
return isIntermediate;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// For versions without this, we look if the route is a `.loading` or `.error` route
|
|
173
|
+
// This is not perfect and may false-positive in some cases, but it's the best we can do
|
|
174
|
+
return transition.to?.localName === 'loading' || transition.to?.localName === 'error';
|
|
175
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { subscribe } from '@ember/instrumentation';
|
|
2
|
+
import { scheduleOnce } from '@ember/runloop';
|
|
3
|
+
import type { EmberRunQueues } from '@ember/runloop/-private/types';
|
|
4
|
+
import { getActiveSpan, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/browser';
|
|
5
|
+
import type { Span } from '@sentry/core';
|
|
6
|
+
import { browserPerformanceTimeOrigin, timestampInSeconds } from '@sentry/core';
|
|
7
|
+
import { getBackburner } from './performance';
|
|
8
|
+
|
|
9
|
+
type Payload = {
|
|
10
|
+
containerKey: string;
|
|
11
|
+
initialRender: true;
|
|
12
|
+
object: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type RenderEntry = {
|
|
16
|
+
payload: Payload;
|
|
17
|
+
now: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
interface RenderEntries {
|
|
21
|
+
[name: string]: RenderEntry;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** This is global, so should only be run once in tests! */
|
|
25
|
+
export function instrumentGlobalsForPerformance(config: {
|
|
26
|
+
disableRunloopPerformance?: boolean;
|
|
27
|
+
minimumRunloopQueueDuration?: number;
|
|
28
|
+
disableInstrumentComponents?: boolean;
|
|
29
|
+
minimumComponentRenderDuration?: number;
|
|
30
|
+
enableComponentDefinitions?: boolean;
|
|
31
|
+
disableInitialLoadInstrumentation?: boolean;
|
|
32
|
+
}): void {
|
|
33
|
+
const {
|
|
34
|
+
disableRunloopPerformance,
|
|
35
|
+
minimumRunloopQueueDuration,
|
|
36
|
+
disableInstrumentComponents,
|
|
37
|
+
minimumComponentRenderDuration,
|
|
38
|
+
enableComponentDefinitions,
|
|
39
|
+
disableInitialLoadInstrumentation,
|
|
40
|
+
} = config;
|
|
41
|
+
|
|
42
|
+
if (!disableRunloopPerformance) {
|
|
43
|
+
_instrumentEmberRunloop({
|
|
44
|
+
minimumRunloopQueueDuration,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (!disableInstrumentComponents) {
|
|
48
|
+
_instrumentComponents({
|
|
49
|
+
minimumComponentRenderDuration,
|
|
50
|
+
enableComponentDefinitions,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (!disableInitialLoadInstrumentation) {
|
|
54
|
+
_instrumentInitialLoad();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function _instrumentEmberRunloop(config: { minimumRunloopQueueDuration?: number }): void {
|
|
59
|
+
const { minimumRunloopQueueDuration } = config;
|
|
60
|
+
let currentQueueStart: number | undefined;
|
|
61
|
+
let currentQueueSpan: Span | undefined;
|
|
62
|
+
const instrumentedEmberQueues = [
|
|
63
|
+
'actions',
|
|
64
|
+
'routerTransitions',
|
|
65
|
+
'render',
|
|
66
|
+
'afterRender',
|
|
67
|
+
'destroy',
|
|
68
|
+
] as EmberRunQueues[];
|
|
69
|
+
|
|
70
|
+
getBackburner().on('begin', (_: unknown, previousInstance: unknown) => {
|
|
71
|
+
if (previousInstance) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const activeSpan = getActiveSpan();
|
|
75
|
+
if (!activeSpan) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (currentQueueSpan) {
|
|
79
|
+
currentQueueSpan.end();
|
|
80
|
+
}
|
|
81
|
+
currentQueueStart = timestampInSeconds();
|
|
82
|
+
|
|
83
|
+
const processQueue = (queue: EmberRunQueues): void => {
|
|
84
|
+
// Process this queue using the end of the previous queue.
|
|
85
|
+
if (currentQueueStart) {
|
|
86
|
+
const now = timestampInSeconds();
|
|
87
|
+
const minQueueDuration = minimumRunloopQueueDuration ?? 5;
|
|
88
|
+
|
|
89
|
+
if ((now - currentQueueStart) * 1000 >= minQueueDuration) {
|
|
90
|
+
startInactiveSpan({
|
|
91
|
+
attributes: {
|
|
92
|
+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember',
|
|
93
|
+
},
|
|
94
|
+
name: 'runloop',
|
|
95
|
+
op: `ui.ember.runloop.${queue}`,
|
|
96
|
+
startTime: currentQueueStart,
|
|
97
|
+
onlyIfParent: true,
|
|
98
|
+
})?.end(now);
|
|
99
|
+
}
|
|
100
|
+
currentQueueStart = undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Setup for next queue
|
|
104
|
+
|
|
105
|
+
const stillActiveSpan = getActiveSpan();
|
|
106
|
+
if (!stillActiveSpan) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
currentQueueStart = timestampInSeconds();
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
instrumentedEmberQueues.forEach(queue => {
|
|
113
|
+
scheduleOnce(queue, null, processQueue, queue);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
getBackburner().on('end', (_: unknown, nextInstance: unknown) => {
|
|
117
|
+
if (nextInstance) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (currentQueueSpan) {
|
|
121
|
+
currentQueueSpan.end();
|
|
122
|
+
currentQueueSpan = undefined;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function processComponentRenderBefore(payload: Payload, beforeEntries: RenderEntries): void {
|
|
128
|
+
const info = {
|
|
129
|
+
payload,
|
|
130
|
+
now: timestampInSeconds(),
|
|
131
|
+
};
|
|
132
|
+
beforeEntries[payload.object] = info;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function processComponentRenderAfter(
|
|
136
|
+
payload: Payload,
|
|
137
|
+
beforeEntries: RenderEntries,
|
|
138
|
+
op: string,
|
|
139
|
+
minComponentDuration: number,
|
|
140
|
+
): void {
|
|
141
|
+
const begin = beforeEntries[payload.object];
|
|
142
|
+
|
|
143
|
+
if (!begin) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const now = timestampInSeconds();
|
|
148
|
+
const componentRenderDuration = now - begin.now;
|
|
149
|
+
|
|
150
|
+
if (componentRenderDuration * 1000 >= minComponentDuration) {
|
|
151
|
+
startInactiveSpan({
|
|
152
|
+
name: payload.containerKey || payload.object,
|
|
153
|
+
op,
|
|
154
|
+
startTime: begin.now,
|
|
155
|
+
attributes: {
|
|
156
|
+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember',
|
|
157
|
+
},
|
|
158
|
+
onlyIfParent: true,
|
|
159
|
+
})?.end(now);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function _instrumentComponents(config: {
|
|
164
|
+
minimumComponentRenderDuration?: number;
|
|
165
|
+
enableComponentDefinitions?: boolean;
|
|
166
|
+
}): void {
|
|
167
|
+
const { minimumComponentRenderDuration, enableComponentDefinitions } = config;
|
|
168
|
+
|
|
169
|
+
const minComponentDuration = minimumComponentRenderDuration ?? 2;
|
|
170
|
+
|
|
171
|
+
const beforeEntries = {} as RenderEntries;
|
|
172
|
+
const beforeComponentDefinitionEntries = {} as RenderEntries;
|
|
173
|
+
|
|
174
|
+
function _subscribeToRenderEvents(): void {
|
|
175
|
+
subscribe('render.component', {
|
|
176
|
+
before(_name: string, _timestamp: number, payload: Payload) {
|
|
177
|
+
processComponentRenderBefore(payload, beforeEntries);
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
after(_name: string, _timestamp: number, payload: Payload, _beganIndex: number) {
|
|
181
|
+
processComponentRenderAfter(payload, beforeEntries, 'ui.ember.component.render', minComponentDuration);
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
if (enableComponentDefinitions) {
|
|
185
|
+
subscribe('render.getComponentDefinition', {
|
|
186
|
+
before(_name: string, _timestamp: number, payload: Payload) {
|
|
187
|
+
processComponentRenderBefore(payload, beforeComponentDefinitionEntries);
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
after(_name: string, _timestamp: number, payload: Payload, _beganIndex: number) {
|
|
191
|
+
processComponentRenderAfter(payload, beforeComponentDefinitionEntries, 'ui.ember.component.definition', 0);
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
_subscribeToRenderEvents();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function _instrumentInitialLoad(): void {
|
|
200
|
+
const startName = '@sentry/ember:initial-load-start';
|
|
201
|
+
const endName = '@sentry/ember:initial-load-end';
|
|
202
|
+
|
|
203
|
+
const { HAS_PERFORMANCE, HAS_PERFORMANCE_TIMING } = _hasPerformanceSupport();
|
|
204
|
+
|
|
205
|
+
if (!HAS_PERFORMANCE) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const { performance } = window;
|
|
210
|
+
|
|
211
|
+
const origin = browserPerformanceTimeOrigin();
|
|
212
|
+
// Split performance check in two so clearMarks still happens even if timeOrigin isn't available.
|
|
213
|
+
if (!HAS_PERFORMANCE_TIMING || origin === undefined) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const measureName = '@sentry/ember:initial-load';
|
|
217
|
+
|
|
218
|
+
const startMarkExists = performance.getEntriesByName(startName).length > 0;
|
|
219
|
+
const endMarkExists = performance.getEntriesByName(endName).length > 0;
|
|
220
|
+
if (!startMarkExists || !endMarkExists) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
performance.measure(measureName, startName, endName);
|
|
225
|
+
const measures = performance.getEntriesByName(measureName);
|
|
226
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
227
|
+
const measure = measures[0]!;
|
|
228
|
+
|
|
229
|
+
const startTime = (measure.startTime + origin) / 1000;
|
|
230
|
+
const endTime = startTime + measure.duration / 1000;
|
|
231
|
+
|
|
232
|
+
startInactiveSpan({
|
|
233
|
+
op: 'ui.ember.init',
|
|
234
|
+
name: 'init',
|
|
235
|
+
attributes: {
|
|
236
|
+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember',
|
|
237
|
+
},
|
|
238
|
+
startTime,
|
|
239
|
+
onlyIfParent: true,
|
|
240
|
+
})?.end(endTime);
|
|
241
|
+
performance.clearMarks(startName);
|
|
242
|
+
performance.clearMarks(endName);
|
|
243
|
+
|
|
244
|
+
performance.clearMeasures(measureName);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function _hasPerformanceSupport(): { HAS_PERFORMANCE: boolean; HAS_PERFORMANCE_TIMING: boolean } {
|
|
248
|
+
// TS says that all of these methods are always available, but some of them may not be supported in older browsers
|
|
249
|
+
// So we "pretend" they are all optional in order to be able to check this properly without TS complaining
|
|
250
|
+
const _performance = window.performance as {
|
|
251
|
+
clearMarks?: Performance['clearMarks'];
|
|
252
|
+
clearMeasures?: Performance['clearMeasures'];
|
|
253
|
+
measure?: Performance['measure'];
|
|
254
|
+
getEntriesByName?: Performance['getEntriesByName'];
|
|
255
|
+
};
|
|
256
|
+
const HAS_PERFORMANCE = Boolean(_performance?.clearMarks && _performance.clearMeasures);
|
|
257
|
+
const HAS_PERFORMANCE_TIMING = Boolean(
|
|
258
|
+
_performance.measure && _performance.getEntriesByName && browserPerformanceTimeOrigin !== undefined,
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
HAS_PERFORMANCE,
|
|
263
|
+
HAS_PERFORMANCE_TIMING,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type ApplicationInstance from '@ember/application/instance';
|
|
2
|
+
import { _backburner, run } from '@ember/runloop';
|
|
3
|
+
import { getOwnConfig, importSync, isTesting, macroCondition } from '@embroider/macros';
|
|
4
|
+
import { getClient } from '@sentry/browser';
|
|
5
|
+
import { addIntegration, GLOBAL_OBJ } from '@sentry/core';
|
|
6
|
+
import type { ExtendedBackburner } from '@sentry/ember/runloop';
|
|
7
|
+
import type { EmberSentryConfig, GlobalConfig, OwnConfig } from '../types';
|
|
8
|
+
import type { browserTracingIntegration as browserTracingIntegrationType } from './browserTracingIntegration';
|
|
9
|
+
|
|
10
|
+
export function getSentryConfig(): EmberSentryConfig {
|
|
11
|
+
const _global = GLOBAL_OBJ as typeof GLOBAL_OBJ & GlobalConfig;
|
|
12
|
+
_global.__sentryEmberConfig = _global.__sentryEmberConfig ?? {};
|
|
13
|
+
const environmentConfig = getOwnConfig<OwnConfig>().sentryConfig;
|
|
14
|
+
if (!environmentConfig.sentry) {
|
|
15
|
+
environmentConfig.sentry = {
|
|
16
|
+
browserTracingOptions: {},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
Object.assign(environmentConfig.sentry, _global.__sentryEmberConfig);
|
|
20
|
+
return environmentConfig;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getBackburner(): Pick<ExtendedBackburner, 'on' | 'off'> {
|
|
24
|
+
if (_backburner) {
|
|
25
|
+
return _backburner as unknown as Pick<ExtendedBackburner, 'on' | 'off'>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if ((run as unknown as { backburner?: Pick<ExtendedBackburner, 'on' | 'off'> }).backburner) {
|
|
29
|
+
return (run as unknown as { backburner: Pick<ExtendedBackburner, 'on' | 'off'> }).backburner;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
on() {
|
|
34
|
+
// noop
|
|
35
|
+
},
|
|
36
|
+
off() {
|
|
37
|
+
// noop
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Utility to register the browser tracing integration and instrument the app instance for performance.
|
|
44
|
+
*/
|
|
45
|
+
export function instrumentForPerformance(appInstance: ApplicationInstance): void {
|
|
46
|
+
const config = getSentryConfig();
|
|
47
|
+
// Maintaining backwards compatibility with config.browserTracingOptions, but passing it with Sentry options is preferred.
|
|
48
|
+
const browserTracingOptions = config.browserTracingOptions || config.sentry.browserTracingOptions || {};
|
|
49
|
+
|
|
50
|
+
const { browserTracingIntegration } = importSync('./browserTracingIntegration') as {
|
|
51
|
+
browserTracingIntegration: typeof browserTracingIntegrationType;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const idleTimeout = config.transitionTimeout || 5000;
|
|
55
|
+
|
|
56
|
+
const emberSpecificConfig = {
|
|
57
|
+
minimumRunloopQueueDuration: config.minimumRunloopQueueDuration,
|
|
58
|
+
minimumComponentRenderDuration: config.minimumComponentRenderDuration,
|
|
59
|
+
enableComponentDefinitions: config.enableComponentDefinitions,
|
|
60
|
+
disableInitialLoadInstrumentation: config.disableInitialLoadInstrumentation,
|
|
61
|
+
disableRunloopPerformance: config.disableRunloopPerformance,
|
|
62
|
+
disableInstrumentComponents: config.disableInstrumentComponents,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const browserTracing = browserTracingIntegration({
|
|
66
|
+
appInstance,
|
|
67
|
+
idleTimeout,
|
|
68
|
+
...browserTracingOptions,
|
|
69
|
+
...emberSpecificConfig,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const client = getClient();
|
|
73
|
+
const isAlreadyInitialized = macroCondition(isTesting()) ? client?.getIntegrationByName('BrowserTracing') : false;
|
|
74
|
+
addIntegration(browserTracing);
|
|
75
|
+
|
|
76
|
+
// Ensure this is re-run in tests even if the integration is already initialized
|
|
77
|
+
if (isAlreadyInitialized && client) {
|
|
78
|
+
browserTracing.afterAllSetup?.(client);
|
|
79
|
+
}
|
|
80
|
+
}
|
package/index.d.ts
CHANGED
|
@@ -8,3 +8,7 @@ export declare function init(_runtimeConfig?: BrowserOptions): Client | undefine
|
|
|
8
8
|
type RouteConstructor = new (...args: ConstructorParameters<typeof Route>) => Route;
|
|
9
9
|
export declare const instrumentRoutePerformance: <T extends RouteConstructor>(BaseRoute: T) => T;
|
|
10
10
|
export * from '@sentry/browser';
|
|
11
|
+
/**
|
|
12
|
+
* Ember-specific browser tracing integration
|
|
13
|
+
*/
|
|
14
|
+
export { browserTracingIntegration } from './utils/browserTracingIntegration';
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import type ApplicationInstance from '@ember/application/instance';
|
|
2
|
-
import type RouterService from '@ember/routing/router-service';
|
|
3
|
-
import type { startBrowserTracingNavigationSpan as startBrowserTracingNavigationSpanType, startBrowserTracingPageLoadSpan as startBrowserTracingPageLoadSpanType } from '@sentry/browser';
|
|
4
|
-
import type { EmberRouterMain, EmberSentryConfig } from '../types';
|
|
5
2
|
export declare function initialize(appInstance: ApplicationInstance): void;
|
|
6
|
-
export declare function _getLocationURL(location: EmberRouterMain['location']): string;
|
|
7
|
-
export declare function _instrumentEmberRouter(routerService: RouterService, routerMain: EmberRouterMain, config: EmberSentryConfig, startBrowserTracingPageLoadSpan: typeof startBrowserTracingPageLoadSpanType, startBrowserTracingNavigationSpan: typeof startBrowserTracingNavigationSpanType): void;
|
|
8
|
-
export declare function instrumentForPerformance(appInstance: ApplicationInstance): Promise<void>;
|
|
9
3
|
declare const _default: {
|
|
10
4
|
initialize: typeof initialize;
|
|
5
|
+
name: string;
|
|
11
6
|
};
|
|
12
7
|
export default _default;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentry/ember",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.54.0",
|
|
4
4
|
"description": "Official Sentry SDK for Ember.js",
|
|
5
5
|
"repository": "git://github.com/getsentry/sentry-javascript.git",
|
|
6
6
|
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/ember",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@babel/core": "^7.27.7",
|
|
34
34
|
"@embroider/macros": "^1.16.0",
|
|
35
|
-
"@sentry/browser": "10.
|
|
36
|
-
"@sentry/core": "10.
|
|
35
|
+
"@sentry/browser": "10.54.0",
|
|
36
|
+
"@sentry/core": "10.54.0",
|
|
37
37
|
"ember-auto-import": "^2.7.2",
|
|
38
38
|
"ember-cli-babel": "^8.2.0",
|
|
39
39
|
"ember-cli-htmlbars": "^6.1.1",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { browserTracingIntegration as originalBrowserTracingIntegration } from '@sentry/browser';
|
|
2
|
+
import { type Integration } from '@sentry/core';
|
|
3
|
+
import type ApplicationInstance from '@ember/application/instance';
|
|
4
|
+
type EmberBrowserTracingIntegrationOptions = Parameters<typeof originalBrowserTracingIntegration>[0] & {
|
|
5
|
+
appInstance?: ApplicationInstance;
|
|
6
|
+
disableRunloopPerformance?: boolean;
|
|
7
|
+
minimumRunloopQueueDuration?: number;
|
|
8
|
+
disableInstrumentComponents?: boolean;
|
|
9
|
+
minimumComponentRenderDuration?: number;
|
|
10
|
+
enableComponentDefinitions?: boolean;
|
|
11
|
+
disableInitialLoadInstrumentation?: boolean;
|
|
12
|
+
};
|
|
13
|
+
export declare function browserTracingIntegration(options: EmberBrowserTracingIntegrationOptions): Integration;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type ApplicationInstance from '@ember/application/instance';
|
|
2
|
+
import type { startBrowserTracingNavigationSpan as startBrowserTracingNavigationSpanType, startBrowserTracingPageLoadSpan as startBrowserTracingPageLoadSpanType } from '@sentry/browser';
|
|
3
|
+
import type { Client } from '@sentry/core';
|
|
4
|
+
import type { EmberRouterMain } from '../types';
|
|
5
|
+
export declare function instrumentEmberAppInstanceForPerformance(client: Client, appInstance: ApplicationInstance, config: {
|
|
6
|
+
disableRunloopPerformance?: boolean;
|
|
7
|
+
instrumentPageLoad?: boolean;
|
|
8
|
+
instrumentNavigation?: boolean;
|
|
9
|
+
}, startBrowserTracingPageLoadSpan: typeof startBrowserTracingPageLoadSpanType, startBrowserTracingNavigationSpan: typeof startBrowserTracingNavigationSpanType): void;
|
|
10
|
+
export declare function _getLocationURL(location: EmberRouterMain['location']): string;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** This is global, so should only be run once in tests! */
|
|
2
|
+
export declare function instrumentGlobalsForPerformance(config: {
|
|
3
|
+
disableRunloopPerformance?: boolean;
|
|
4
|
+
minimumRunloopQueueDuration?: number;
|
|
5
|
+
disableInstrumentComponents?: boolean;
|
|
6
|
+
minimumComponentRenderDuration?: number;
|
|
7
|
+
enableComponentDefinitions?: boolean;
|
|
8
|
+
disableInitialLoadInstrumentation?: boolean;
|
|
9
|
+
}): void;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type ApplicationInstance from '@ember/application/instance';
|
|
2
|
+
import type { ExtendedBackburner } from '@sentry/ember/runloop';
|
|
3
|
+
import type { EmberSentryConfig } from '../types';
|
|
4
|
+
export declare function getSentryConfig(): EmberSentryConfig;
|
|
5
|
+
export declare function getBackburner(): Pick<ExtendedBackburner, 'on' | 'off'>;
|
|
6
|
+
/**
|
|
7
|
+
* Utility to register the browser tracing integration and instrument the app instance for performance.
|
|
8
|
+
*/
|
|
9
|
+
export declare function instrumentForPerformance(appInstance: ApplicationInstance): void;
|