@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.
Files changed (47) 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/declarations/utils/instrumentEmberGlobals.d.ts +22 -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 +569 -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 +28 -30
  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/instrumentEmberGlobals.d.ts +0 -9
  45. package/utils/performance.d.ts +0 -9
  46. package/vendor/initial-load-body.js +0 -3
  47. 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/-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 {
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
- // 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 & {
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
- 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;
@@ -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
- interface RenderEntries {
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]: BROWSER_UI_TASK_SPAN_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 processComponentRenderBefore(payload: Payload, beforeEntries: RenderEntries): void {
131
- const info = {
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 processComponentRenderAfter(
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[payload.object];
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 = {} as RenderEntries;
178
- const beforeComponentDefinitionEntries = {} as RenderEntries;
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: Payload) {
183
- processComponentRenderBefore(payload, beforeEntries);
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: Payload, _beganIndex: number) {
187
- 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);
188
187
  },
189
188
  });
190
189
  if (enableComponentDefinitions) {
191
- subscribe('render.getComponentDefinition', {
192
- before(_name: string, _timestamp: number, payload: Payload) {
193
- processComponentRenderBefore(payload, beforeComponentDefinitionEntries);
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: Payload, _beganIndex: number) {
197
- processComponentRenderAfter(payload, beforeComponentDefinitionEntries, GENERAL_FUNCTION_SPAN_OP, 0);
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
- // TODO(v11): Replace with the `ui.mount` constant from `@sentry/conventions/op` once it is registered there.
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
@@ -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.