@sentry/ember 11.0.0-alpha.1 → 11.0.0-beta.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.
Files changed (46) hide show
  1. package/README.md +97 -115
  2. package/addon-main.cjs +4 -0
  3. package/declarations/index.d.ts +10 -0
  4. package/declarations/index.d.ts.map +1 -0
  5. package/declarations/init.d.ts +23 -0
  6. package/declarations/init.d.ts.map +1 -0
  7. package/{utils → declarations/utils}/browserTracingIntegration.d.ts +7 -2
  8. package/declarations/utils/browserTracingIntegration.d.ts.map +1 -0
  9. package/{utils → declarations/utils}/instrumentEmberAppInstanceForPerformance.d.ts +11 -3
  10. package/declarations/utils/instrumentEmberAppInstanceForPerformance.d.ts.map +1 -0
  11. package/{utils → declarations/utils}/instrumentEmberGlobals.d.ts +1 -0
  12. package/declarations/utils/instrumentEmberGlobals.d.ts.map +1 -0
  13. package/declarations/utils/instrumentRoutePerformance.d.ts +28 -0
  14. package/declarations/utils/instrumentRoutePerformance.d.ts.map +1 -0
  15. package/declarations/utils/utils.d.ts +7 -0
  16. package/declarations/utils/utils.d.ts.map +1 -0
  17. package/dist/index.js +568 -0
  18. package/dist/index.js.map +1 -0
  19. package/package.json +74 -71
  20. package/src/index.ts +13 -0
  21. package/src/init.ts +32 -0
  22. package/{addon → src}/utils/browserTracingIntegration.ts +40 -30
  23. package/{addon → src}/utils/instrumentEmberAppInstanceForPerformance.ts +180 -141
  24. package/{addon → src}/utils/instrumentEmberGlobals.ts +17 -16
  25. package/src/utils/instrumentRoutePerformance.ts +79 -0
  26. package/src/utils/utils.ts +25 -0
  27. package/.oxlintrc.json +0 -13
  28. package/LICENSE +0 -21
  29. package/addon/index.ts +0 -117
  30. package/addon/instance-initializers/sentry-performance.ts +0 -23
  31. package/addon/runloop.d.ts +0 -19
  32. package/addon/types.ts +0 -39
  33. package/addon/utils/performance.ts +0 -80
  34. package/app/instance-initializers/sentry-performance.js +0 -1
  35. package/config/environment.js +0 -5
  36. package/index.d.ts +0 -14
  37. package/index.js +0 -115
  38. package/instance-initializers/sentry-performance.d.ts +0 -7
  39. package/runloop.d.ts +0 -19
  40. package/tsconfig.json +0 -25
  41. package/types/dummy/index.d.ts +0 -0
  42. package/types/global.d.ts +0 -15
  43. package/types.d.ts +0 -36
  44. package/utils/performance.d.ts +0 -9
  45. package/vendor/initial-load-body.js +0 -3
  46. package/vendor/initial-load-head.js +0 -3
@@ -1,20 +1,39 @@
1
1
  import type ApplicationInstance from '@ember/application/instance';
2
- import type Transition from '@ember/routing/-private/transition';
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
- getAbsoluteUrl,
10
- SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
11
- SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
12
- startInactiveSpan,
13
- } from '@sentry/browser';
14
- import type { Client, Span } from '@sentry/core';
15
- import type { EmberRouterMain } from '../types';
16
- import { getBackburner } from './performance';
17
- import { SENTRY_OP, URL_FULL, URL_PATH, URL_TEMPLATE } from '@sentry/conventions/attributes';
10
+ SENTRY_SEGMENT_NAME_SOURCE,
11
+ SENTRY_OP,
12
+ URL_FULL,
13
+ URL_PATH,
14
+ URL_TEMPLATE,
15
+ } from '@sentry/conventions/attributes';
16
+ import { ROUTER } from '@sentry/conventions/op';
17
+ import {
18
+ filterCollectedUrl,
19
+ getCurrentScope,
20
+ hasSpanStreamingEnabled,
21
+ PAGELOAD_SPAN_NAME_FALLBACK,
22
+ ROUTER_SPAN_NAME_FALLBACK,
23
+ spanToJSON,
24
+ type Client,
25
+ type Span,
26
+ } from '@sentry/core';
27
+ import { getBackburner } from './utils.ts';
28
+
29
+ interface EmberRouterMain {
30
+ location: {
31
+ formatURL?: (url: string) => string;
32
+ getURL?: () => string;
33
+ implementation?: string;
34
+ rootURL: string;
35
+ };
36
+ }
18
37
 
19
38
  type TransitionWithIntent = Transition & { intent?: { url?: string } };
20
39
 
@@ -25,36 +44,159 @@ export function instrumentEmberAppInstanceForPerformance(
25
44
  startBrowserTracingPageLoadSpan: typeof startBrowserTracingPageLoadSpanType,
26
45
  startBrowserTracingNavigationSpan: typeof startBrowserTracingNavigationSpanType,
27
46
  ): void {
28
- // eslint-disable-next-line ember/no-private-routing-service
29
- const routerMain = appInstance.lookup('router:main') as EmberRouterMain;
30
- let routerService = appInstance.lookup('service:router') as RouterService & {
47
+ const { disableRunloopPerformance, instrumentPageLoad, instrumentNavigation } = config;
48
+ const routerService = getRouterService(appInstance);
49
+
50
+ if (!routerService.recognize) {
51
+ // Router is missing critical functionality to limit cardinality of the transaction names.
52
+ return;
53
+ }
54
+
55
+ const routerMain = getRouterMain(appInstance);
56
+ const location = routerMain.location;
57
+ let activeRootSpan: Span | undefined;
58
+ let transitionSpan: Span | undefined;
59
+
60
+ const url = _getLocationURL(location);
61
+
62
+ if (instrumentPageLoad !== false) {
63
+ // Somehow the router service etc. may not be fully ready/initialized yet at this point
64
+ // Probably because we are running this before the Ember setup is necessarily completed
65
+ // So in order to accomodate this, we fall back to starting the pageload span with the current URL and update it later
66
+ const routeInfo = url ? routerService.recognize(url) : undefined;
67
+
68
+ activeRootSpan = startBrowserTracingPageLoadSpan(client, {
69
+ // With span streaming, span names have to be low cardinality, so we can't fall back to the URL.
70
+ name: routeInfo
71
+ ? `route:${routeInfo.name}`
72
+ : hasSpanStreamingEnabled(client)
73
+ ? PAGELOAD_SPAN_NAME_FALLBACK
74
+ : url || WINDOW.location.pathname,
75
+ attributes: {
76
+ [SENTRY_SEGMENT_NAME_SOURCE]: routeInfo ? 'route' : 'url',
77
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.ember',
78
+ ...(url ? _getRouteUrlAttributes(client, url, routeInfo?.params) : {}),
79
+ toRoute: routeInfo?.name,
80
+ },
81
+ });
82
+ }
83
+
84
+ const finishActiveTransaction = (_: unknown, nextInstance: unknown): void => {
85
+ if (nextInstance) {
86
+ return;
87
+ }
88
+ activeRootSpan?.end();
89
+ getBackburner().off('end', finishActiveTransaction);
90
+ };
91
+
92
+ routerService.on('routeWillChange', (transition: Transition) => {
93
+ const { fromRoute, toRoute } = getTransitionInformation(transition, routerService);
94
+
95
+ // Store this here to be used, even if the active span has ended
96
+ getCurrentScope().setTransactionName(`route:${toRoute}`);
97
+
98
+ // We want to ignore loading && error routes
99
+ if (transitionIsIntermediate(transition)) {
100
+ return;
101
+ }
102
+
103
+ // If this is not the initial transition, we want to end the active root span and start a new one
104
+ if (fromRoute != null) {
105
+ activeRootSpan?.end();
106
+
107
+ if (instrumentNavigation !== false) {
108
+ // Only `intent.url` reliably reflects the *destination* URL at `routeWillChange` time. The
109
+ // router's location still points at the current (pre-transition) route here, so falling back to
110
+ // it would tag the navigation span with the previous route's `url.*` attributes. When we don't
111
+ // have a trustworthy target URL, we omit them and let `routeDidChange` set them from `currentURL`.
112
+ const targetUrl = (transition as TransitionWithIntent).intent?.url;
113
+ const urlAttributes = targetUrl ? _getRouteUrlAttributes(client, targetUrl, transition.to?.params) : {};
114
+
115
+ activeRootSpan = startBrowserTracingNavigationSpan(client, {
116
+ name: `route:${toRoute}`,
117
+ attributes: {
118
+ [SENTRY_SEGMENT_NAME_SOURCE]: 'route',
119
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.ember',
120
+ ...urlAttributes,
121
+ fromRoute,
122
+ toRoute,
123
+ },
124
+ });
125
+ }
126
+ } else if (activeRootSpan && spanToJSON(activeRootSpan).attributes[SENTRY_SEGMENT_NAME_SOURCE] === 'url') {
127
+ // We make sure to update the pageload span with the current URL, if we couldn't get it before
128
+ // In this case we re-load the router:main reference, as this may change and we may have a stale reference
129
+ const location = getRouterMain(appInstance).location;
130
+ const url = _getLocationURL(location);
131
+ if (url) {
132
+ const routeInfo = routerService.recognize(url);
133
+ activeRootSpan.updateName(`route:${toRoute}`);
134
+ activeRootSpan.setAttributes({
135
+ [SENTRY_SEGMENT_NAME_SOURCE]: 'route',
136
+ ..._getRouteUrlAttributes(client, url, routeInfo?.params),
137
+ toRoute: toRoute,
138
+ });
139
+ }
140
+ }
141
+
142
+ // transition spans are only emitted if instrumentNavigation is true
143
+ if (instrumentNavigation === false) {
144
+ return;
145
+ }
146
+
147
+ transitionSpan = startInactiveSpan({
148
+ attributes: {
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
- routerService = routerService.externalRouter;
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._hasMountedSentryPerformanceRouting = true;
50
- _instrumentEmberRouter(
51
- client,
52
- routerService,
53
- routerMain,
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 || router.currentRouteName;
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
- // Only exported for testing
102
- export function _getRouteUrlAttributes(
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 { BROWSER_UI_RENDER_SPAN_OP, BROWSER_UI_TASK_SPAN_OP, GENERAL_FUNCTION_SPAN_OP } from '@sentry/conventions/op';
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 './performance';
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;
@@ -88,7 +90,7 @@ function _instrumentEmberRunloop(config: { minimumRunloopQueueDuration?: number
88
90
  if ((now - currentQueueStart) * 1000 >= minQueueDuration) {
89
91
  startInactiveSpan({
90
92
  attributes: {
91
- [SENTRY_OP]: BROWSER_UI_TASK_SPAN_OP,
93
+ [SENTRY_OP]: UI_TASK,
92
94
  [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember',
93
95
  'ember.runloop.queue': queue,
94
96
  },
@@ -175,23 +177,23 @@ function _instrumentComponents(config: {
175
177
  const beforeComponentDefinitionEntries: RenderEntries = new WeakMap();
176
178
 
177
179
  function _subscribeToRenderEvents(): void {
178
- subscribe('render.component', {
179
- before(_name: string, _timestamp: number, payload: Payload) {
180
- _processComponentRenderBefore(payload, beforeEntries);
180
+ subscribe<void>('render.component', {
181
+ before(_name: string, _timestamp: number, payload: object) {
182
+ _processComponentRenderBefore(payload as Payload, beforeEntries);
181
183
  },
182
184
 
183
- after(_name: string, _timestamp: number, payload: Payload, _beganIndex: number) {
184
- _processComponentRenderAfter(payload, beforeEntries, BROWSER_UI_RENDER_SPAN_OP, minComponentDuration);
185
+ after(_name: string, _timestamp: number, payload: object) {
186
+ _processComponentRenderAfter(payload as Payload, beforeEntries, UI_RENDER, minComponentDuration);
185
187
  },
186
188
  });
187
189
  if (enableComponentDefinitions) {
188
- subscribe('render.getComponentDefinition', {
189
- before(_name: string, _timestamp: number, payload: Payload) {
190
- _processComponentRenderBefore(payload, beforeComponentDefinitionEntries);
190
+ subscribe<void>('render.getComponentDefinition', {
191
+ before(_name: string, _timestamp: number, payload: object) {
192
+ _processComponentRenderBefore(payload as Payload, beforeComponentDefinitionEntries);
191
193
  },
192
194
 
193
- after(_name: string, _timestamp: number, payload: Payload, _beganIndex: number) {
194
- _processComponentRenderAfter(payload, beforeComponentDefinitionEntries, GENERAL_FUNCTION_SPAN_OP, 0);
195
+ after(_name: string, _timestamp: number, payload: object) {
196
+ _processComponentRenderAfter(payload as Payload, beforeComponentDefinitionEntries, FUNCTION, 0);
195
197
  },
196
198
  });
197
199
  }
@@ -235,8 +237,7 @@ function _instrumentInitialLoad(): void {
235
237
  startInactiveSpan({
236
238
  name: 'init',
237
239
  attributes: {
238
- // TODO(v11): Replace with the `ui.mount` constant from `@sentry/conventions/op` once it is registered there.
239
- [SENTRY_OP]: 'ui.mount',
240
+ [SENTRY_OP]: UI_MOUNT,
240
241
  [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember',
241
242
  },
242
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
@@ -1,13 +0,0 @@
1
- {
2
- "$schema": "../../node_modules/oxlint/configuration_schema.json",
3
- "extends": ["../../.oxlintrc.base.json"],
4
- "overrides": [
5
- {
6
- "files": ["vendor/**/*.js"],
7
- "env": {
8
- "browser": true,
9
- "node": false
10
- }
11
- }
12
- ]
13
- }
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.