@sentry/ember 11.0.0-alpha.0 → 11.0.0-alpha.2
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/README.md +97 -115
- package/addon-main.cjs +4 -0
- package/declarations/index.d.ts +10 -0
- package/declarations/index.d.ts.map +1 -0
- package/declarations/init.d.ts +23 -0
- package/declarations/init.d.ts.map +1 -0
- package/{utils → declarations/utils}/browserTracingIntegration.d.ts +7 -2
- package/declarations/utils/browserTracingIntegration.d.ts.map +1 -0
- package/{utils → declarations/utils}/instrumentEmberAppInstanceForPerformance.d.ts +11 -3
- package/declarations/utils/instrumentEmberAppInstanceForPerformance.d.ts.map +1 -0
- package/declarations/utils/instrumentEmberGlobals.d.ts +22 -0
- package/declarations/utils/instrumentEmberGlobals.d.ts.map +1 -0
- package/declarations/utils/instrumentRoutePerformance.d.ts +28 -0
- package/declarations/utils/instrumentRoutePerformance.d.ts.map +1 -0
- package/declarations/utils/utils.d.ts +7 -0
- package/declarations/utils/utils.d.ts.map +1 -0
- package/dist/index.js +569 -0
- package/dist/index.js.map +1 -0
- package/package.json +74 -71
- package/src/index.ts +13 -0
- package/src/init.ts +32 -0
- package/{addon → src}/utils/browserTracingIntegration.ts +40 -30
- package/{addon → src}/utils/instrumentEmberAppInstanceForPerformance.ts +180 -141
- package/{addon → src}/utils/instrumentEmberGlobals.ts +28 -30
- package/src/utils/instrumentRoutePerformance.ts +79 -0
- package/src/utils/utils.ts +25 -0
- package/.oxlintrc.json +0 -13
- package/LICENSE +0 -21
- package/addon/index.ts +0 -117
- package/addon/instance-initializers/sentry-performance.ts +0 -23
- package/addon/runloop.d.ts +0 -19
- package/addon/types.ts +0 -39
- package/addon/utils/performance.ts +0 -80
- package/app/instance-initializers/sentry-performance.js +0 -1
- package/config/environment.js +0 -5
- package/index.d.ts +0 -14
- package/index.js +0 -115
- package/instance-initializers/sentry-performance.d.ts +0 -7
- package/runloop.d.ts +0 -19
- package/tsconfig.json +0 -25
- package/types/dummy/index.d.ts +0 -0
- package/types/global.d.ts +0 -15
- package/types.d.ts +0 -36
- package/utils/instrumentEmberGlobals.d.ts +0 -9
- package/utils/performance.d.ts +0 -9
- package/vendor/initial-load-body.js +0 -3
- package/vendor/initial-load-head.js +0 -3
|
@@ -1,20 +1,38 @@
|
|
|
1
1
|
import type ApplicationInstance from '@ember/application/instance';
|
|
2
|
-
import type Transition from '@ember/routing
|
|
2
|
+
import type Transition from '@ember/routing/transition';
|
|
3
3
|
import type RouterService from '@ember/routing/router-service';
|
|
4
4
|
import type {
|
|
5
5
|
startBrowserTracingNavigationSpan as startBrowserTracingNavigationSpanType,
|
|
6
6
|
startBrowserTracingPageLoadSpan as startBrowserTracingPageLoadSpanType,
|
|
7
7
|
} from '@sentry/browser';
|
|
8
|
+
import { getAbsoluteUrl, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, WINDOW } from '@sentry/browser';
|
|
8
9
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
import
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
SENTRY_SEGMENT_NAME_SOURCE,
|
|
11
|
+
SENTRY_OP,
|
|
12
|
+
URL_FULL,
|
|
13
|
+
URL_PATH,
|
|
14
|
+
URL_TEMPLATE,
|
|
15
|
+
} from '@sentry/conventions/attributes';
|
|
16
|
+
import {
|
|
17
|
+
filterCollectedUrl,
|
|
18
|
+
getCurrentScope,
|
|
19
|
+
hasSpanStreamingEnabled,
|
|
20
|
+
PAGELOAD_SPAN_NAME_FALLBACK,
|
|
21
|
+
ROUTER_SPAN_NAME_FALLBACK,
|
|
22
|
+
spanToJSON,
|
|
23
|
+
type Client,
|
|
24
|
+
type Span,
|
|
25
|
+
} from '@sentry/core';
|
|
26
|
+
import { getBackburner } from './utils.ts';
|
|
27
|
+
|
|
28
|
+
interface EmberRouterMain {
|
|
29
|
+
location: {
|
|
30
|
+
formatURL?: (url: string) => string;
|
|
31
|
+
getURL?: () => string;
|
|
32
|
+
implementation?: string;
|
|
33
|
+
rootURL: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
18
36
|
|
|
19
37
|
type TransitionWithIntent = Transition & { intent?: { url?: string } };
|
|
20
38
|
|
|
@@ -25,36 +43,160 @@ export function instrumentEmberAppInstanceForPerformance(
|
|
|
25
43
|
startBrowserTracingPageLoadSpan: typeof startBrowserTracingPageLoadSpanType,
|
|
26
44
|
startBrowserTracingNavigationSpan: typeof startBrowserTracingNavigationSpanType,
|
|
27
45
|
): void {
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
46
|
+
const { disableRunloopPerformance, instrumentPageLoad, instrumentNavigation } = config;
|
|
47
|
+
const routerService = getRouterService(appInstance);
|
|
48
|
+
|
|
49
|
+
if (!routerService.recognize) {
|
|
50
|
+
// Router is missing critical functionality to limit cardinality of the transaction names.
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const routerMain = getRouterMain(appInstance);
|
|
55
|
+
const location = routerMain.location;
|
|
56
|
+
let activeRootSpan: Span | undefined;
|
|
57
|
+
let transitionSpan: Span | undefined;
|
|
58
|
+
|
|
59
|
+
const url = _getLocationURL(location);
|
|
60
|
+
|
|
61
|
+
if (instrumentPageLoad !== false) {
|
|
62
|
+
// Somehow the router service etc. may not be fully ready/initialized yet at this point
|
|
63
|
+
// Probably because we are running this before the Ember setup is necessarily completed
|
|
64
|
+
// So in order to accomodate this, we fall back to starting the pageload span with the current URL and update it later
|
|
65
|
+
const routeInfo = url ? routerService.recognize(url) : undefined;
|
|
66
|
+
|
|
67
|
+
activeRootSpan = startBrowserTracingPageLoadSpan(client, {
|
|
68
|
+
// With span streaming, span names have to be low cardinality, so we can't fall back to the URL.
|
|
69
|
+
name: routeInfo
|
|
70
|
+
? `route:${routeInfo.name}`
|
|
71
|
+
: hasSpanStreamingEnabled(client)
|
|
72
|
+
? PAGELOAD_SPAN_NAME_FALLBACK
|
|
73
|
+
: url || WINDOW.location.pathname,
|
|
74
|
+
attributes: {
|
|
75
|
+
[SENTRY_SEGMENT_NAME_SOURCE]: routeInfo ? 'route' : 'url',
|
|
76
|
+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.ember',
|
|
77
|
+
...(url ? _getRouteUrlAttributes(client, url, routeInfo?.params) : {}),
|
|
78
|
+
toRoute: routeInfo?.name,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const finishActiveTransaction = (_: unknown, nextInstance: unknown): void => {
|
|
84
|
+
if (nextInstance) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
activeRootSpan?.end();
|
|
88
|
+
getBackburner().off('end', finishActiveTransaction);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
routerService.on('routeWillChange', (transition: Transition) => {
|
|
92
|
+
const { fromRoute, toRoute } = getTransitionInformation(transition, routerService);
|
|
93
|
+
|
|
94
|
+
// Store this here to be used, even if the active span has ended
|
|
95
|
+
getCurrentScope().setTransactionName(`route:${toRoute}`);
|
|
96
|
+
|
|
97
|
+
// We want to ignore loading && error routes
|
|
98
|
+
if (transitionIsIntermediate(transition)) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// If this is not the initial transition, we want to end the active root span and start a new one
|
|
103
|
+
if (fromRoute != null) {
|
|
104
|
+
activeRootSpan?.end();
|
|
105
|
+
|
|
106
|
+
if (instrumentNavigation !== false) {
|
|
107
|
+
// Only `intent.url` reliably reflects the *destination* URL at `routeWillChange` time. The
|
|
108
|
+
// router's location still points at the current (pre-transition) route here, so falling back to
|
|
109
|
+
// it would tag the navigation span with the previous route's `url.*` attributes. When we don't
|
|
110
|
+
// have a trustworthy target URL, we omit them and let `routeDidChange` set them from `currentURL`.
|
|
111
|
+
const targetUrl = (transition as TransitionWithIntent).intent?.url;
|
|
112
|
+
const urlAttributes = targetUrl ? _getRouteUrlAttributes(client, targetUrl, transition.to?.params) : {};
|
|
113
|
+
|
|
114
|
+
activeRootSpan = startBrowserTracingNavigationSpan(client, {
|
|
115
|
+
name: `route:${toRoute}`,
|
|
116
|
+
attributes: {
|
|
117
|
+
[SENTRY_SEGMENT_NAME_SOURCE]: 'route',
|
|
118
|
+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.ember',
|
|
119
|
+
...urlAttributes,
|
|
120
|
+
fromRoute,
|
|
121
|
+
toRoute,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
} else if (activeRootSpan && spanToJSON(activeRootSpan).attributes[SENTRY_SEGMENT_NAME_SOURCE] === 'url') {
|
|
126
|
+
// We make sure to update the pageload span with the current URL, if we couldn't get it before
|
|
127
|
+
// In this case we re-load the router:main reference, as this may change and we may have a stale reference
|
|
128
|
+
const location = getRouterMain(appInstance).location;
|
|
129
|
+
const url = _getLocationURL(location);
|
|
130
|
+
if (url) {
|
|
131
|
+
const routeInfo = routerService.recognize(url);
|
|
132
|
+
activeRootSpan.updateName(`route:${toRoute}`);
|
|
133
|
+
activeRootSpan.setAttributes({
|
|
134
|
+
[SENTRY_SEGMENT_NAME_SOURCE]: 'route',
|
|
135
|
+
..._getRouteUrlAttributes(client, url, routeInfo?.params),
|
|
136
|
+
toRoute: toRoute,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// transition spans are only emitted if instrumentNavigation is true
|
|
142
|
+
if (instrumentNavigation === false) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
transitionSpan = startInactiveSpan({
|
|
147
|
+
attributes: {
|
|
148
|
+
// TODO(conventions): Replace `'router'` with the `router` span op constant once it is released in `@sentry/conventions`.
|
|
149
|
+
[SENTRY_OP]: 'router',
|
|
150
|
+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember',
|
|
151
|
+
},
|
|
152
|
+
// With span streaming, span names have to be low cardinality, and Ember gives us no route
|
|
153
|
+
// template for the transition itself, so it's the fallback.
|
|
154
|
+
name: hasSpanStreamingEnabled(client) ? ROUTER_SPAN_NAME_FALLBACK : `route:${fromRoute} -> route:${toRoute}`,
|
|
155
|
+
onlyIfParent: true,
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
routerService.on('routeDidChange', transition => {
|
|
160
|
+
if (!transitionSpan || !activeRootSpan || transitionIsIntermediate(transition)) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
transitionSpan.end();
|
|
164
|
+
|
|
165
|
+
const url = routerService.currentURL ?? _getLocationURL(location);
|
|
166
|
+
if (url) {
|
|
167
|
+
const routeInfo = routerService.recognize(url);
|
|
168
|
+
// `currentURL` is the normalized route path and never includes the hash fragment, so we source
|
|
169
|
+
// `url.full` from the location URL (which preserves `#/...` for hash-location apps) when available.
|
|
170
|
+
const fullUrl = _getLocationURL(location) || url;
|
|
171
|
+
activeRootSpan.setAttributes(
|
|
172
|
+
_getRouteUrlAttributes(client, url, routeInfo?.params ?? transition.to?.params, fullUrl),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (disableRunloopPerformance) {
|
|
177
|
+
activeRootSpan.end();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
getBackburner().on('end', finishActiveTransaction);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function getRouterService(appInstance: ApplicationInstance): RouterService {
|
|
186
|
+
const routerService = appInstance.lookup('service:router') as RouterService & {
|
|
31
187
|
externalRouter?: RouterService;
|
|
32
|
-
_hasMountedSentryPerformanceRouting?: boolean;
|
|
33
|
-
currentURL?: string;
|
|
34
188
|
};
|
|
35
189
|
|
|
36
190
|
if (routerService.externalRouter) {
|
|
37
191
|
// Using ember-engines-router-service in an engine.
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
if (routerService._hasMountedSentryPerformanceRouting) {
|
|
41
|
-
// Routing listens to route changes on the main router, and should not be initialized multiple times per page.
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
if (!routerService.recognize) {
|
|
45
|
-
// Router is missing critical functionality to limit cardinality of the transaction names.
|
|
46
|
-
return;
|
|
192
|
+
return routerService.externalRouter;
|
|
47
193
|
}
|
|
48
194
|
|
|
49
|
-
routerService
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
config,
|
|
55
|
-
startBrowserTracingPageLoadSpan,
|
|
56
|
-
startBrowserTracingNavigationSpan,
|
|
57
|
-
);
|
|
195
|
+
return routerService;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function getRouterMain(appInstance: ApplicationInstance): EmberRouterMain {
|
|
199
|
+
return appInstance.lookup('router:main') as EmberRouterMain;
|
|
58
200
|
}
|
|
59
201
|
|
|
60
202
|
function getTransitionInformation(
|
|
@@ -62,7 +204,8 @@ function getTransitionInformation(
|
|
|
62
204
|
router: RouterService,
|
|
63
205
|
): { fromRoute?: string; toRoute?: string } {
|
|
64
206
|
const fromRoute = transition?.from?.name;
|
|
65
|
-
const toRoute = transition?.to?.name
|
|
207
|
+
const toRoute = transition?.to?.name ?? router.currentRouteName ?? undefined;
|
|
208
|
+
|
|
66
209
|
return {
|
|
67
210
|
fromRoute,
|
|
68
211
|
toRoute,
|
|
@@ -98,8 +241,8 @@ function buildUrlTemplate(path: string, params: Record<string, unknown> = {}): s
|
|
|
98
241
|
return template;
|
|
99
242
|
}
|
|
100
243
|
|
|
101
|
-
|
|
102
|
-
|
|
244
|
+
function _getRouteUrlAttributes(
|
|
245
|
+
client: Client,
|
|
103
246
|
url: string,
|
|
104
247
|
params: Record<string, unknown> = {},
|
|
105
248
|
fullUrl: string = url,
|
|
@@ -110,7 +253,7 @@ export function _getRouteUrlAttributes(
|
|
|
110
253
|
// fragment (e.g. `https://host/#/tracing`), which would otherwise be lost by `getUrlPathFromEmberLocation`.
|
|
111
254
|
return {
|
|
112
255
|
[URL_PATH]: path,
|
|
113
|
-
[URL_FULL]: getAbsoluteUrl(fullUrl),
|
|
256
|
+
[URL_FULL]: filterCollectedUrl(getAbsoluteUrl(fullUrl), client),
|
|
114
257
|
[URL_TEMPLATE]: buildUrlTemplate(path, params),
|
|
115
258
|
};
|
|
116
259
|
}
|
|
@@ -129,110 +272,6 @@ export function _getLocationURL(location: EmberRouterMain['location']): string {
|
|
|
129
272
|
return url;
|
|
130
273
|
}
|
|
131
274
|
|
|
132
|
-
function _instrumentEmberRouter(
|
|
133
|
-
client: Client,
|
|
134
|
-
routerService: RouterService & { currentURL?: string },
|
|
135
|
-
routerMain: EmberRouterMain,
|
|
136
|
-
config: { disableRunloopPerformance?: boolean; instrumentPageLoad?: boolean; instrumentNavigation?: boolean },
|
|
137
|
-
startBrowserTracingPageLoadSpan: typeof startBrowserTracingPageLoadSpanType,
|
|
138
|
-
startBrowserTracingNavigationSpan: typeof startBrowserTracingNavigationSpanType,
|
|
139
|
-
): void {
|
|
140
|
-
const { disableRunloopPerformance, instrumentPageLoad, instrumentNavigation } = config;
|
|
141
|
-
const location = routerMain.location;
|
|
142
|
-
let activeRootSpan: Span | undefined;
|
|
143
|
-
let transitionSpan: Span | undefined;
|
|
144
|
-
|
|
145
|
-
const url = _getLocationURL(location);
|
|
146
|
-
|
|
147
|
-
if (url && instrumentPageLoad !== false) {
|
|
148
|
-
const routeInfo = routerService.recognize(url);
|
|
149
|
-
activeRootSpan = startBrowserTracingPageLoadSpan(client, {
|
|
150
|
-
name: `route:${routeInfo.name}`,
|
|
151
|
-
attributes: {
|
|
152
|
-
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
|
|
153
|
-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.ember',
|
|
154
|
-
..._getRouteUrlAttributes(url, routeInfo.params),
|
|
155
|
-
url,
|
|
156
|
-
toRoute: routeInfo.name,
|
|
157
|
-
},
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const finishActiveTransaction = (_: unknown, nextInstance: unknown): void => {
|
|
162
|
-
if (nextInstance) {
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
activeRootSpan?.end();
|
|
166
|
-
getBackburner().off('end', finishActiveTransaction);
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
if (instrumentNavigation === false) {
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
routerService.on('routeWillChange', (transition: Transition) => {
|
|
174
|
-
const { fromRoute, toRoute } = getTransitionInformation(transition, routerService);
|
|
175
|
-
|
|
176
|
-
// We want to ignore loading && error routes
|
|
177
|
-
if (transitionIsIntermediate(transition)) {
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
activeRootSpan?.end();
|
|
182
|
-
|
|
183
|
-
// Only `intent.url` reliably reflects the *destination* URL at `routeWillChange` time. The
|
|
184
|
-
// router's location still points at the current (pre-transition) route here, so falling back to
|
|
185
|
-
// it would tag the navigation span with the previous route's `url.*` attributes. When we don't
|
|
186
|
-
// have a trustworthy target URL, we omit them and let `routeDidChange` set them from `currentURL`.
|
|
187
|
-
const targetUrl = (transition as TransitionWithIntent).intent?.url;
|
|
188
|
-
const urlAttributes = targetUrl ? _getRouteUrlAttributes(targetUrl, transition.to?.params) : {};
|
|
189
|
-
|
|
190
|
-
activeRootSpan = startBrowserTracingNavigationSpan(client, {
|
|
191
|
-
name: `route:${toRoute}`,
|
|
192
|
-
attributes: {
|
|
193
|
-
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
|
|
194
|
-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.ember',
|
|
195
|
-
...urlAttributes,
|
|
196
|
-
fromRoute,
|
|
197
|
-
toRoute,
|
|
198
|
-
},
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
transitionSpan = startInactiveSpan({
|
|
202
|
-
attributes: {
|
|
203
|
-
// TODO(conventions): Replace `'router'` with the `router` span op constant once it is released in `@sentry/conventions`.
|
|
204
|
-
[SENTRY_OP]: 'router',
|
|
205
|
-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember',
|
|
206
|
-
},
|
|
207
|
-
name: `route:${fromRoute} -> route:${toRoute}`,
|
|
208
|
-
onlyIfParent: true,
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
routerService.on('routeDidChange', transition => {
|
|
213
|
-
if (!transitionSpan || !activeRootSpan || transitionIsIntermediate(transition)) {
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
transitionSpan.end();
|
|
217
|
-
|
|
218
|
-
const url = routerService.currentURL ?? _getLocationURL(location);
|
|
219
|
-
if (url) {
|
|
220
|
-
const routeInfo = routerService.recognize(url);
|
|
221
|
-
// `currentURL` is the normalized route path and never includes the hash fragment, so we source
|
|
222
|
-
// `url.full` from the location URL (which preserves `#/...` for hash-location apps) when available.
|
|
223
|
-
const fullUrl = _getLocationURL(location) || url;
|
|
224
|
-
activeRootSpan.setAttributes(_getRouteUrlAttributes(url, routeInfo.params ?? transition.to?.params, fullUrl));
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (disableRunloopPerformance) {
|
|
228
|
-
activeRootSpan.end();
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
getBackburner().on('end', finishActiveTransaction);
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
|
|
236
275
|
function transitionIsIntermediate(transition: Transition): boolean {
|
|
237
276
|
// We want to use ignore, as this may actually be defined on new versions
|
|
238
277
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { subscribe } from '@ember/instrumentation';
|
|
2
2
|
import { scheduleOnce } from '@ember/runloop';
|
|
3
|
-
import type { EmberRunQueues } from '@ember/runloop/-private/types';
|
|
4
3
|
import { SENTRY_OP, UI_COMPONENT_NAME } from '@sentry/conventions/attributes';
|
|
5
|
-
import {
|
|
4
|
+
import { UI_MOUNT, UI_RENDER, UI_TASK, FUNCTION } from '@sentry/conventions/op';
|
|
6
5
|
import { getActiveSpan, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/browser';
|
|
7
6
|
import type { Span } from '@sentry/core';
|
|
8
7
|
import { browserPerformanceTimeOrigin, timestampInSeconds } from '@sentry/core';
|
|
9
|
-
import { getBackburner } from './
|
|
8
|
+
import { getBackburner } from './utils.ts';
|
|
9
|
+
|
|
10
|
+
// Ember runloop queue names
|
|
11
|
+
type EmberRunQueues = 'actions' | 'afterRender' | 'destroy' | 'render' | 'routerTransitions' | 'sync';
|
|
10
12
|
|
|
11
13
|
type Payload = {
|
|
12
14
|
containerKey: string;
|
|
@@ -15,13 +17,10 @@ type Payload = {
|
|
|
15
17
|
};
|
|
16
18
|
|
|
17
19
|
type RenderEntry = {
|
|
18
|
-
payload: Payload;
|
|
19
20
|
now: number;
|
|
20
21
|
};
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
[name: string]: RenderEntry;
|
|
24
|
-
}
|
|
23
|
+
export type RenderEntries = WeakMap<Payload, RenderEntry>;
|
|
25
24
|
|
|
26
25
|
/** This is global, so should only be run once in tests! */
|
|
27
26
|
export function instrumentGlobalsForPerformance(config: {
|
|
@@ -91,7 +90,7 @@ function _instrumentEmberRunloop(config: { minimumRunloopQueueDuration?: number
|
|
|
91
90
|
if ((now - currentQueueStart) * 1000 >= minQueueDuration) {
|
|
92
91
|
startInactiveSpan({
|
|
93
92
|
attributes: {
|
|
94
|
-
[SENTRY_OP]:
|
|
93
|
+
[SENTRY_OP]: UI_TASK,
|
|
95
94
|
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember',
|
|
96
95
|
'ember.runloop.queue': queue,
|
|
97
96
|
},
|
|
@@ -127,26 +126,26 @@ function _instrumentEmberRunloop(config: { minimumRunloopQueueDuration?: number
|
|
|
127
126
|
});
|
|
128
127
|
}
|
|
129
128
|
|
|
130
|
-
function
|
|
131
|
-
|
|
132
|
-
payload,
|
|
133
|
-
now: timestampInSeconds(),
|
|
134
|
-
};
|
|
135
|
-
beforeEntries[payload.object] = info;
|
|
129
|
+
export function _processComponentRenderBefore(payload: Payload, beforeEntries: RenderEntries): void {
|
|
130
|
+
beforeEntries.set(payload, { now: timestampInSeconds() });
|
|
136
131
|
}
|
|
137
132
|
|
|
138
|
-
function
|
|
133
|
+
export function _processComponentRenderAfter(
|
|
139
134
|
payload: Payload,
|
|
140
135
|
beforeEntries: RenderEntries,
|
|
141
136
|
op: string,
|
|
142
137
|
minComponentDuration: number,
|
|
143
138
|
): void {
|
|
144
|
-
const begin = beforeEntries
|
|
139
|
+
const begin = beforeEntries.get(payload);
|
|
145
140
|
|
|
146
141
|
if (!begin) {
|
|
147
142
|
return;
|
|
148
143
|
}
|
|
149
144
|
|
|
145
|
+
// A WeakMap entry cannot outlive its payload, but delete promptly anyway
|
|
146
|
+
// so a long-lived payload doesn't keep the entry around between renders.
|
|
147
|
+
beforeEntries.delete(payload);
|
|
148
|
+
|
|
150
149
|
const now = timestampInSeconds();
|
|
151
150
|
const componentRenderDuration = now - begin.now;
|
|
152
151
|
|
|
@@ -174,27 +173,27 @@ function _instrumentComponents(config: {
|
|
|
174
173
|
|
|
175
174
|
const minComponentDuration = minimumComponentRenderDuration ?? 2;
|
|
176
175
|
|
|
177
|
-
const beforeEntries =
|
|
178
|
-
const beforeComponentDefinitionEntries =
|
|
176
|
+
const beforeEntries: RenderEntries = new WeakMap();
|
|
177
|
+
const beforeComponentDefinitionEntries: RenderEntries = new WeakMap();
|
|
179
178
|
|
|
180
179
|
function _subscribeToRenderEvents(): void {
|
|
181
|
-
subscribe('render.component', {
|
|
182
|
-
before(_name: string, _timestamp: number, payload:
|
|
183
|
-
|
|
180
|
+
subscribe<void>('render.component', {
|
|
181
|
+
before(_name: string, _timestamp: number, payload: object) {
|
|
182
|
+
_processComponentRenderBefore(payload as Payload, beforeEntries);
|
|
184
183
|
},
|
|
185
184
|
|
|
186
|
-
after(_name: string, _timestamp: number, payload:
|
|
187
|
-
|
|
185
|
+
after(_name: string, _timestamp: number, payload: object) {
|
|
186
|
+
_processComponentRenderAfter(payload as Payload, beforeEntries, UI_RENDER, minComponentDuration);
|
|
188
187
|
},
|
|
189
188
|
});
|
|
190
189
|
if (enableComponentDefinitions) {
|
|
191
|
-
subscribe('render.getComponentDefinition', {
|
|
192
|
-
before(_name: string, _timestamp: number, payload:
|
|
193
|
-
|
|
190
|
+
subscribe<void>('render.getComponentDefinition', {
|
|
191
|
+
before(_name: string, _timestamp: number, payload: object) {
|
|
192
|
+
_processComponentRenderBefore(payload as Payload, beforeComponentDefinitionEntries);
|
|
194
193
|
},
|
|
195
194
|
|
|
196
|
-
after(_name: string, _timestamp: number, payload:
|
|
197
|
-
|
|
195
|
+
after(_name: string, _timestamp: number, payload: object) {
|
|
196
|
+
_processComponentRenderAfter(payload as Payload, beforeComponentDefinitionEntries, FUNCTION, 0);
|
|
198
197
|
},
|
|
199
198
|
});
|
|
200
199
|
}
|
|
@@ -238,8 +237,7 @@ function _instrumentInitialLoad(): void {
|
|
|
238
237
|
startInactiveSpan({
|
|
239
238
|
name: 'init',
|
|
240
239
|
attributes: {
|
|
241
|
-
|
|
242
|
-
[SENTRY_OP]: 'ui.mount',
|
|
240
|
+
[SENTRY_OP]: UI_MOUNT,
|
|
243
241
|
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember',
|
|
244
242
|
},
|
|
245
243
|
startTime,
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { startSpan } from '@sentry/browser';
|
|
2
|
+
import { CODE_FUNCTION_NAME, SENTRY_OP } from '@sentry/conventions/attributes';
|
|
3
|
+
import { FUNCTION } from '@sentry/conventions/op';
|
|
4
|
+
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
|
|
5
|
+
|
|
6
|
+
import type Route from '@ember/routing/route';
|
|
7
|
+
|
|
8
|
+
type RouteConstructor = new (...args: ConstructorParameters<typeof Route>) => Route;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Enables monitoring the performance of an Ember app.
|
|
12
|
+
*
|
|
13
|
+
* This wraps the route's lifecycle hooks (beforeModel, model, afterModel, setupController)
|
|
14
|
+
* with Sentry spans to track their performance.
|
|
15
|
+
*
|
|
16
|
+
* @param BaseRoute - The Route class to instrument
|
|
17
|
+
* @returns The instrumented Route class
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* import Route from '@ember/routing/route';
|
|
22
|
+
* import { instrumentRoutePerformance } from '@sentry/ember';
|
|
23
|
+
*
|
|
24
|
+
* class ApplicationRoute extends Route {
|
|
25
|
+
* async model() {
|
|
26
|
+
* return this.store.findAll('post');
|
|
27
|
+
* }
|
|
28
|
+
* }
|
|
29
|
+
*
|
|
30
|
+
* export default instrumentRoutePerformance(ApplicationRoute);
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function instrumentRoutePerformance<T extends RouteConstructor>(BaseRoute: T): T {
|
|
34
|
+
const instrumentFunction = async (
|
|
35
|
+
hookName: string,
|
|
36
|
+
name: string,
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Route hooks have varied signatures that can't be unified with unknown
|
|
38
|
+
fn: (...args: any[]) => any,
|
|
39
|
+
args: unknown[],
|
|
40
|
+
): Promise<unknown> => {
|
|
41
|
+
return startSpan(
|
|
42
|
+
{
|
|
43
|
+
attributes: {
|
|
44
|
+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember',
|
|
45
|
+
[SENTRY_OP]: FUNCTION,
|
|
46
|
+
[CODE_FUNCTION_NAME]: hookName,
|
|
47
|
+
},
|
|
48
|
+
name,
|
|
49
|
+
onlyIfParent: true,
|
|
50
|
+
},
|
|
51
|
+
() => {
|
|
52
|
+
return fn(...args);
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const routeName = BaseRoute.name;
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
// @ts-expect-error TS2545 We do not need to redefine a constructor here
|
|
61
|
+
[routeName]: class extends BaseRoute {
|
|
62
|
+
public beforeModel(...args: unknown[]): void | Promise<unknown> {
|
|
63
|
+
return instrumentFunction('beforeModel', this.fullRouteName, super.beforeModel.bind(this), args);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public async model(...args: unknown[]): Promise<unknown> {
|
|
67
|
+
return instrumentFunction('model', this.fullRouteName, super.model.bind(this), args);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public afterModel(...args: unknown[]): void | Promise<unknown> {
|
|
71
|
+
return instrumentFunction('afterModel', this.fullRouteName, super.afterModel.bind(this), args);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public setupController(...args: unknown[]): void | Promise<unknown> {
|
|
75
|
+
return instrumentFunction('setupController', this.fullRouteName, super.setupController.bind(this), args);
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
}[routeName] as T;
|
|
79
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { _backburner, run } from '@ember/runloop';
|
|
2
|
+
|
|
3
|
+
interface ExtendedBackburner {
|
|
4
|
+
on(eventName: string, callback: (...args: unknown[]) => void): void;
|
|
5
|
+
off(eventName: string, callback: (...args: unknown[]) => void): void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function getBackburner(): Pick<ExtendedBackburner, 'on' | 'off'> {
|
|
9
|
+
if (_backburner) {
|
|
10
|
+
return _backburner;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if ((run as unknown as { backburner?: Pick<ExtendedBackburner, 'on' | 'off'> }).backburner) {
|
|
14
|
+
return (run as unknown as { backburner: Pick<ExtendedBackburner, 'on' | 'off'> }).backburner;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
on() {
|
|
19
|
+
// noop
|
|
20
|
+
},
|
|
21
|
+
off() {
|
|
22
|
+
// noop
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
package/.oxlintrc.json
DELETED
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2020 Functional Software, Inc. dba Sentry
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
-
this software and associated documentation files (the "Software"), to deal in
|
|
7
|
-
the Software without restriction, including without limitation the rights to
|
|
8
|
-
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
9
|
-
of the Software, and to permit persons to whom the Software is furnished to do
|
|
10
|
-
so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|