Skip to content

Commit 705bcf0

Browse files
committed
move stuff around and fix it
1 parent 5c76a25 commit 705bcf0

7 files changed

Lines changed: 502 additions & 439 deletions

File tree

packages/ember/addon/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,8 @@ export const instrumentRoutePerformance = <T extends RouteConstructor>(BaseRoute
119119
};
120120

121121
export * from '@sentry/browser';
122+
123+
/**
124+
* Ember-specific browser tracing integration
125+
*/
126+
export { browserTracingIntegration } from './utils/browserTracingIntegration';

packages/ember/addon/instance-initializers/sentry-performance.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable max-lines */
22
import type ApplicationInstance from '@ember/application/instance';
3-
import { getOwnConfig, isTesting, macroCondition } from '@embroider/macros';
3+
import { getOwnConfig } from '@embroider/macros';
44
import { GLOBAL_OBJ } from '@sentry/core';
55
import type { EmberSentryConfig, GlobalConfig, OwnConfig } from '../types';
66
import { instrumentForPerformance } from '../utils/performance';
@@ -30,7 +30,6 @@ export function initialize(appInstance: ApplicationInstance): void {
3030
return;
3131
}
3232
instrumentForPerformance(appInstance);
33-
3433
}
3534

3635
export default {

packages/ember/addon/utils/browserTracingIntegration.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,22 @@ import {
55
} from '@sentry/browser';
66
import type { Integration } from '@sentry/core';
77
import type ApplicationInstance from '@ember/application/instance';
8-
import { instrumentEmberAppInstanceForPerformance } from './performance';
8+
import { instrumentEmberAppInstanceForPerformance } from './instrumentEmberAppInstanceForPerformance';
9+
import { instrumentGlobalsForPerformance } from './instrumentEmberGlobals';
10+
import { isTesting, macroCondition } from '@embroider/macros';
911

1012
type EmberBrowserTracingIntegrationOptions = Parameters<typeof originalBrowserTracingIntegration>[0] & {
1113
appInstance: ApplicationInstance;
14+
disableRunloopPerformance?: boolean;
15+
minimumRunloopQueueDuration?: number;
16+
disableInstrumentComponents?: boolean;
17+
minimumComponentRenderDuration?: number;
18+
enableComponentDefinitions?: boolean;
19+
disableInitialLoadInstrumentation?: boolean;
1220
};
1321

22+
let _initialized = false;
23+
1424
export function browserTracingIntegration(options: EmberBrowserTracingIntegrationOptions): Integration {
1525
const { appInstance } = options;
1626

@@ -23,10 +33,19 @@ export function browserTracingIntegration(options: EmberBrowserTracingIntegratio
2333
instrumentPageLoad: false,
2434
});
2535

26-
const config = {
27-
disableRunloopPerformance: false,
36+
const appInstancePerformanceConfig = {
37+
disableRunloopPerformance: options.disableRunloopPerformance ?? false,
2838
instrumentPageLoad,
29-
instrumentNavigation
39+
instrumentNavigation,
40+
};
41+
42+
const globalsPerformanceConfig = {
43+
disableRunloopPerformance: options.disableRunloopPerformance ?? false,
44+
minimumRunloopQueueDuration: options.minimumRunloopQueueDuration ?? 0,
45+
disableInstrumentComponents: options.disableInstrumentComponents ?? false,
46+
minimumComponentRenderDuration: options.minimumComponentRenderDuration ?? 0,
47+
enableComponentDefinitions: options.enableComponentDefinitions ?? false,
48+
disableInitialLoadInstrumentation: options.disableInitialLoadInstrumentation ?? false,
3049
};
3150

3251
return {
@@ -36,10 +55,20 @@ export function browserTracingIntegration(options: EmberBrowserTracingIntegratio
3655

3756
instrumentEmberAppInstanceForPerformance(
3857
appInstance,
39-
config,
58+
appInstancePerformanceConfig,
4059
startBrowserTracingPageLoadSpan,
4160
startBrowserTracingNavigationSpan,
4261
);
62+
63+
// We only want to run this once in tests!
64+
if (macroCondition(isTesting())) {
65+
if (_initialized) {
66+
return;
67+
}
68+
}
69+
70+
instrumentGlobalsForPerformance(globalsPerformanceConfig);
71+
_initialized = true;
4372
},
4473
};
4574
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
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+
BrowserClient,
6+
startBrowserTracingNavigationSpan as startBrowserTracingNavigationSpanType,
7+
startBrowserTracingPageLoadSpan as startBrowserTracingPageLoadSpanType,
8+
} from '@sentry/browser';
9+
import {
10+
getClient,
11+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
12+
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
13+
startInactiveSpan,
14+
} from '@sentry/browser';
15+
import type { Span } from '@sentry/core';
16+
import type { EmberRouterMain } from '../types';
17+
import { getBackburner } from './performance';
18+
19+
export function instrumentEmberAppInstanceForPerformance(
20+
appInstance: ApplicationInstance,
21+
config: { disableRunloopPerformance?: boolean; instrumentPageLoad?: boolean; instrumentNavigation?: boolean },
22+
startBrowserTracingPageLoadSpan: typeof startBrowserTracingPageLoadSpanType,
23+
startBrowserTracingNavigationSpan: typeof startBrowserTracingNavigationSpanType,
24+
): void {
25+
// eslint-disable-next-line ember/no-private-routing-service
26+
const routerMain = appInstance.lookup('router:main') as EmberRouterMain;
27+
let routerService = appInstance.lookup('service:router') as RouterService & {
28+
externalRouter?: RouterService;
29+
_hasMountedSentryPerformanceRouting?: boolean;
30+
};
31+
32+
if (routerService.externalRouter) {
33+
// Using ember-engines-router-service in an engine.
34+
routerService = routerService.externalRouter;
35+
}
36+
if (routerService._hasMountedSentryPerformanceRouting) {
37+
// Routing listens to route changes on the main router, and should not be initialized multiple times per page.
38+
return;
39+
}
40+
if (!routerService.recognize) {
41+
// Router is missing critical functionality to limit cardinality of the transaction names.
42+
return;
43+
}
44+
45+
routerService._hasMountedSentryPerformanceRouting = true;
46+
_instrumentEmberRouter(
47+
routerService,
48+
routerMain,
49+
config,
50+
startBrowserTracingPageLoadSpan,
51+
startBrowserTracingNavigationSpan,
52+
);
53+
}
54+
55+
function getTransitionInformation(
56+
transition: Transition | undefined,
57+
router: RouterService,
58+
): { fromRoute?: string; toRoute?: string } {
59+
const fromRoute = transition?.from?.name;
60+
const toRoute = transition?.to?.name || router.currentRouteName;
61+
return {
62+
fromRoute,
63+
toRoute,
64+
};
65+
}
66+
67+
// Only exported for testing
68+
export function _getLocationURL(location: EmberRouterMain['location']): string {
69+
if (!location?.getURL || !location?.formatURL) {
70+
return '';
71+
}
72+
const url = location.formatURL(location.getURL());
73+
74+
// `implementation` is optional in Ember's predefined location types, so we also check if the URL starts with '#'.
75+
if (location.implementation === 'hash' || url.startsWith('#')) {
76+
return `${location.rootURL}${url}`;
77+
}
78+
return url;
79+
}
80+
81+
function _instrumentEmberRouter(
82+
routerService: RouterService,
83+
routerMain: EmberRouterMain,
84+
config: { disableRunloopPerformance?: boolean; instrumentPageLoad?: boolean; instrumentNavigation?: boolean },
85+
startBrowserTracingPageLoadSpan: typeof startBrowserTracingPageLoadSpanType,
86+
startBrowserTracingNavigationSpan: typeof startBrowserTracingNavigationSpanType,
87+
): void {
88+
const { disableRunloopPerformance, instrumentPageLoad, instrumentNavigation } = config;
89+
const location = routerMain.location;
90+
let activeRootSpan: Span | undefined;
91+
let transitionSpan: Span | undefined;
92+
93+
const url = _getLocationURL(location);
94+
95+
const client = getClient<BrowserClient>();
96+
97+
if (!client) {
98+
return;
99+
}
100+
101+
if (url && instrumentPageLoad !== false) {
102+
const routeInfo = routerService.recognize(url);
103+
activeRootSpan = startBrowserTracingPageLoadSpan(client, {
104+
name: `route:${routeInfo.name}`,
105+
attributes: {
106+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
107+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.ember',
108+
url,
109+
toRoute: routeInfo.name,
110+
},
111+
});
112+
}
113+
114+
const finishActiveTransaction = (_: unknown, nextInstance: unknown): void => {
115+
if (nextInstance) {
116+
return;
117+
}
118+
activeRootSpan?.end();
119+
getBackburner().off('end', finishActiveTransaction);
120+
};
121+
122+
if (instrumentNavigation === false) {
123+
return;
124+
}
125+
126+
routerService.on('routeWillChange', (transition: Transition) => {
127+
const { fromRoute, toRoute } = getTransitionInformation(transition, routerService);
128+
129+
// We want to ignore loading && error routes
130+
if (transitionIsIntermediate(transition)) {
131+
return;
132+
}
133+
134+
activeRootSpan?.end();
135+
136+
activeRootSpan = startBrowserTracingNavigationSpan(client, {
137+
name: `route:${toRoute}`,
138+
attributes: {
139+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
140+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.ember',
141+
fromRoute,
142+
toRoute,
143+
},
144+
});
145+
146+
transitionSpan = startInactiveSpan({
147+
attributes: {
148+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember',
149+
},
150+
op: 'ui.ember.transition',
151+
name: `route:${fromRoute} -> route:${toRoute}`,
152+
onlyIfParent: true,
153+
});
154+
});
155+
156+
routerService.on('routeDidChange', transition => {
157+
if (!transitionSpan || !activeRootSpan || transitionIsIntermediate(transition)) {
158+
return;
159+
}
160+
transitionSpan.end();
161+
162+
if (disableRunloopPerformance) {
163+
activeRootSpan.end();
164+
return;
165+
}
166+
167+
getBackburner().on('end', finishActiveTransaction);
168+
});
169+
}
170+
171+
function transitionIsIntermediate(transition: Transition): boolean {
172+
// We want to use ignore, as this may actually be defined on new versions
173+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
174+
// @ts-ignore This actually exists on newer versions
175+
const isIntermediate: boolean | undefined = transition.isIntermediate;
176+
177+
if (typeof isIntermediate === 'boolean') {
178+
return isIntermediate;
179+
}
180+
181+
// For versions without this, we look if the route is a `.loading` or `.error` route
182+
// This is not perfect and may false-positive in some cases, but it's the best we can do
183+
return transition.to?.localName === 'loading' || transition.to?.localName === 'error';
184+
}

0 commit comments

Comments
 (0)