@sentry/ember 7.3.0 → 7.4.1

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 CHANGED
@@ -21,28 +21,41 @@ This package is an Ember addon that wraps `@sentry/browser`, with added function
21
21
  As with other Ember addons, run:
22
22
  `ember install @sentry/ember`
23
23
 
24
- Then add the following config to `config/environment.js`
24
+ Then add the following to your `<your-app>/app.js`
25
25
 
26
26
  ```javascript
27
- ENV['@sentry/ember'] = {
28
- sentry: {
29
- dsn: '__DSN__' // replace __DSN__ with your DSN,
30
- tracesSampleRate: 1.0, // Be sure to lower this for your production environment
31
- }
32
- };
27
+ import * as Sentry from "@sentry/ember";
28
+
29
+ Sentry.init({
30
+ dsn: '__DSN__' // replace __DSN__ with your DSN,
31
+
32
+ // Set tracesSampleRate to 1.0 to capture 100%
33
+ // of transactions for performance monitoring.
34
+ // We recommend adjusting this value in production,
35
+ tracesSampleRate: 1.0,
36
+ });
33
37
  ```
38
+
34
39
  ### Usage
35
40
 
36
- To use this SDK, call `InitSentryForEmber` before the application is initialized, in `app.js`. This will load Sentry config from `environment.js` for you.
41
+ To use this SDK, call `Sentry.init` before the application is initialized, in `app.js`. This will allow Sentry to capture information while your app is starting.
42
+ Any additional SDK settings can be modified via the usual config in `environment.js` for you, see the Additional Configuration section for more details.
37
43
 
38
44
  ```javascript
39
45
  import Application from '@ember/application';
40
46
  import Resolver from 'ember-resolver';
41
47
  import loadInitializers from 'ember-load-initializers';
42
48
  import config from './config/environment';
43
- import { InitSentryForEmber } from '@sentry/ember';
49
+ import * as Sentry from "@sentry/ember";
44
50
 
45
- InitSentryForEmber();
51
+ Sentry.init({
52
+ dsn: '__DSN__' // replace __DSN__ with your DSN,
53
+
54
+ // Set tracesSampleRate to 1.0 to capture 100%
55
+ // of transactions for performance monitoring.
56
+ // We recommend adjusting this value in production,
57
+ tracesSampleRate: 1.0,
58
+ });
46
59
 
47
60
  export default class App extends Application {
48
61
  modulePrefix = config.modulePrefix;
@@ -75,27 +88,12 @@ ENV['@sentry/ember'] = {
75
88
 
76
89
  // All component definitions will be added as spans.
77
90
  enableComponentDefinition: true,
78
-
79
- // See sentry-javascript configuration https://docs.sentry.io/error-reporting/configuration/?platform=javascript
80
- sentry: {}
81
91
  };
82
92
  ```
83
93
 
84
- You can also pass additional configuration for sentry-javascript directly to the `InitSentryForEmber` method.
85
- This configuration will be merged with `ENV['@sentry/ember'].sentry`:
86
-
87
- ```javascript
88
- InitSentryForEmber({
89
- ignoreErrors: [
90
- /You appear to be offline/,
91
- ],
92
- })
93
- ```
94
-
95
- It is recommended to pass all static sentry-javascript configuration directly to `InitSentryForEmber`, and only keeping configuration that depends on the build environment/secrets in `config/environment.js`. Please note that due to how the environment config is serialized, any non-JSON-serializable config (like a regex) will not work properly when being kept in `config/environment.js`.
96
-
97
94
  #### Disabling Performance
98
95
 
96
+
99
97
  `@sentry/ember` captures performance by default, if you would like to disable the automatic performance instrumentation, you can add the following to your `config/environment.js`:
100
98
 
101
99
  ```javascript
@@ -106,7 +104,9 @@ ENV['@sentry/ember'] = {
106
104
 
107
105
 
108
106
  ### Performance
107
+
109
108
  #### Routes
109
+
110
110
  If you would like to capture `beforeModel`, `model`, `afterModel` and `setupController` times for one of your routes,
111
111
  you can import `instrumentRoutePerformance` and wrap your route with it.
112
112
 
@@ -124,11 +124,13 @@ export default instrumentRoutePerformance(MyRoute);
124
124
  ```
125
125
 
126
126
  #### Runloop
127
+
127
128
  The runloop queue durations are instrumented by default, as long as they are longer than a threshold (by default 5ms).
128
129
  This helps (via the render queue) capturing the entire render in case component render times aren't fully instrumented,
129
130
  such as when using glimmer components.
130
131
 
131
132
  If you would like to change the runloop queue threshold, add the following to your config:
133
+
132
134
  ```javascript
133
135
  ENV['@sentry/ember'] = {
134
136
  minimumRunloopQueueDuration: 0, // All runloop queue durations will be added as spans.
@@ -136,9 +138,11 @@ ENV['@sentry/ember'] = {
136
138
  ```
137
139
 
138
140
  #### Components
141
+
139
142
  Non-glimmer component render times will automatically get captured.
140
143
 
141
144
  If you would like to disable component render being instrumented, add the following to your config:
145
+
142
146
  ```javascript
143
147
  ENV['@sentry/ember'] = {
144
148
  disableInstrumentComponents: true, // Will disable automatic instrumentation for components.
@@ -147,6 +151,7 @@ ENV['@sentry/ember'] = {
147
151
 
148
152
  Additionally, components whose render time is below a threshold (by default 2ms) will not be included as spans.
149
153
  If you would like to change this threshold, add the following to your config:
154
+
150
155
  ```javascript
151
156
  ENV['@sentry/ember'] = {
152
157
  minimumComponentRenderDuration: 0, // All (non-glimmer) component render durations will be added as spans.
@@ -154,9 +159,11 @@ ENV['@sentry/ember'] = {
154
159
  ```
155
160
 
156
161
  #### Glimmer components
162
+
157
163
  Currently glimmer component render durations can only be captured indirectly via the runloop instrumentation. You can
158
164
  optionally enable a setting to show component definitions (which will indicate which components are being rendered) be
159
165
  adding the following to your config:
166
+
160
167
  ```javascript
161
168
  ENV['@sentry/ember'] = {
162
169
  enableComponentDefinition: true, // All component definitions will be added as spans.
@@ -165,7 +172,7 @@ ENV['@sentry/ember'] = {
165
172
 
166
173
  ### Supported Versions
167
174
 
168
- `@sentry/ember` currently supports Ember **3.8+** for error monitoring.
175
+ `@sentry/ember` currently supports Ember **4.0+** for error monitoring.
169
176
 
170
177
  ### Previous Integration
171
178
 
@@ -174,8 +181,8 @@ this Ember addon to offer more Ember-specific error and performancing monitoring
174
181
 
175
182
  ## Testing
176
183
 
177
- You can find example instrumentation in the `dummy` application, which is also used for testing. To test with the dummy
178
- application, you must pass the dsn as an environment variable.
184
+ For this package itself, you can find example instrumentation in the `dummy` application, which is also used for testing.
185
+ To test with the dummy application, you must pass the dsn as an environment variable.
179
186
 
180
187
  ```javascript
181
188
  SENTRY_DSN=__DSN__ ember serve
@@ -0,0 +1,6 @@
1
+ import Ember from 'ember';
2
+ declare module 'ember' {
3
+ namespace Ember {
4
+ export function subscribe(pattern: string, object: {}): any;
5
+ }
6
+ }
package/addon/index.ts ADDED
@@ -0,0 +1,122 @@
1
+ import * as Sentry from '@sentry/browser';
2
+ import { SDK_VERSION, BrowserOptions } from '@sentry/browser';
3
+ import { macroCondition, isDevelopingApp, getOwnConfig } from '@embroider/macros';
4
+ import { next } from '@ember/runloop';
5
+ import { assert, warn } from '@ember/debug';
6
+ import Ember from 'ember';
7
+ import { timestampWithMs } from '@sentry/utils';
8
+ import { GlobalConfig, OwnConfig } from './types';
9
+ import { getGlobalObject } from '@sentry/utils';
10
+
11
+ function _getSentryInitConfig() {
12
+ const _global = getGlobalObject<GlobalConfig>();
13
+ _global.__sentryEmberConfig = _global.__sentryEmberConfig ?? {};
14
+ return _global.__sentryEmberConfig;
15
+ }
16
+
17
+ export function InitSentryForEmber(_runtimeConfig?: BrowserOptions) {
18
+ const environmentConfig = getOwnConfig<OwnConfig>().sentryConfig;
19
+
20
+ assert('Missing configuration.', environmentConfig);
21
+ assert('Missing configuration for Sentry.', environmentConfig.sentry || _runtimeConfig);
22
+
23
+ if (!environmentConfig.sentry) {
24
+ // If environment config is not specified but the above assertion passes, use runtime config.
25
+ environmentConfig.sentry = { ..._runtimeConfig } as any;
26
+ }
27
+
28
+ // Merge runtime config into environment config, preferring runtime.
29
+ Object.assign(environmentConfig.sentry, _runtimeConfig || {});
30
+ const initConfig = Object.assign({}, environmentConfig.sentry);
31
+
32
+ initConfig._metadata = initConfig._metadata || {};
33
+ initConfig._metadata.sdk = {
34
+ name: 'sentry.javascript.ember',
35
+ packages: [
36
+ {
37
+ name: 'npm:@sentry/ember',
38
+ version: SDK_VERSION,
39
+ },
40
+ ],
41
+ version: SDK_VERSION,
42
+ };
43
+
44
+ // Persist Sentry init options so they are identical when performance initializers call init again.
45
+ const sentryInitConfig = _getSentryInitConfig();
46
+ Object.assign(sentryInitConfig, initConfig);
47
+
48
+ Sentry.init(initConfig);
49
+
50
+ if (macroCondition(isDevelopingApp())) {
51
+ if (environmentConfig.ignoreEmberOnErrorWarning) {
52
+ return;
53
+ }
54
+ next(null, function () {
55
+ warn(
56
+ 'Ember.onerror found. Using Ember.onerror can hide some errors (such as flushed runloop errors) from Sentry. Use Sentry.captureException to capture errors within Ember.onError or remove it to have errors caught by Sentry directly. This error can be silenced via addon configuration.',
57
+ !Ember.onerror,
58
+ {
59
+ id: '@sentry/ember.ember-onerror-detected',
60
+ },
61
+ );
62
+ });
63
+ }
64
+ }
65
+
66
+ export const getActiveTransaction = () => {
67
+ return Sentry.getCurrentHub()?.getScope()?.getTransaction();
68
+ };
69
+
70
+ export const instrumentRoutePerformance = (BaseRoute: any) => {
71
+ const instrumentFunction = async (op: string, description: string, fn: Function, args: any) => {
72
+ const startTimestamp = timestampWithMs();
73
+ const result = await fn(...args);
74
+
75
+ const currentTransaction = getActiveTransaction();
76
+ if (!currentTransaction) {
77
+ return result;
78
+ }
79
+ currentTransaction.startChild({ op, description, startTimestamp }).finish();
80
+ return result;
81
+ };
82
+
83
+ return {
84
+ [BaseRoute.name]: class extends BaseRoute {
85
+ beforeModel(...args: any[]) {
86
+ return instrumentFunction(
87
+ 'ember.route.beforeModel',
88
+ (<any>this).fullRouteName,
89
+ super.beforeModel.bind(this),
90
+ args,
91
+ );
92
+ }
93
+
94
+ async model(...args: any[]) {
95
+ return instrumentFunction('ember.route.model', (<any>this).fullRouteName, super.model.bind(this), args);
96
+ }
97
+
98
+ async afterModel(...args: any[]) {
99
+ return instrumentFunction(
100
+ 'ember.route.afterModel',
101
+ (<any>this).fullRouteName,
102
+ super.afterModel.bind(this),
103
+ args,
104
+ );
105
+ }
106
+
107
+ async setupController(...args: any[]) {
108
+ return instrumentFunction(
109
+ 'ember.route.setupController',
110
+ (<any>this).fullRouteName,
111
+ super.setupController.bind(this),
112
+ args,
113
+ );
114
+ }
115
+ },
116
+ }[BaseRoute.name];
117
+ };
118
+
119
+ export * from '@sentry/browser';
120
+
121
+ // init is now the preferred way to call initialization for this addon.
122
+ export const init = InitSentryForEmber;
@@ -0,0 +1,412 @@
1
+ import ApplicationInstance from '@ember/application/instance';
2
+ import Ember from 'ember';
3
+ import { run, _backburner, scheduleOnce } from '@ember/runloop';
4
+ import * as Sentry from '@sentry/browser';
5
+ import { ExtendedBackburner } from '@sentry/ember/runloop';
6
+ import { Span, Transaction, Integration } from '@sentry/types';
7
+ import { EmberRunQueues } from '@ember/runloop/-private/types';
8
+ import { getActiveTransaction } from '..';
9
+ import { browserPerformanceTimeOrigin, getGlobalObject, timestampWithMs } from '@sentry/utils';
10
+ import { macroCondition, isTesting, getOwnConfig } from '@embroider/macros';
11
+ import { EmberSentryConfig, GlobalConfig, OwnConfig } from '../types';
12
+
13
+ function getSentryConfig() {
14
+ const _global = getGlobalObject<GlobalConfig>();
15
+ _global.__sentryEmberConfig = _global.__sentryEmberConfig ?? {};
16
+ const environmentConfig = getOwnConfig<OwnConfig>().sentryConfig;
17
+ if (!environmentConfig.sentry) {
18
+ environmentConfig.sentry = {
19
+ browserTracingOptions: {},
20
+ };
21
+ }
22
+ Object.assign(environmentConfig.sentry, _global.__sentryEmberConfig);
23
+ return environmentConfig;
24
+ }
25
+
26
+ export function initialize(appInstance: ApplicationInstance): void {
27
+ const config = getSentryConfig();
28
+ if (config['disablePerformance']) {
29
+ return;
30
+ }
31
+ const performancePromise = instrumentForPerformance(appInstance);
32
+ if (macroCondition(isTesting())) {
33
+ (<any>window)._sentryPerformanceLoad = performancePromise;
34
+ }
35
+ }
36
+
37
+ function getBackburner(): Pick<ExtendedBackburner, 'on' | 'off'> {
38
+ if (_backburner) {
39
+ return _backburner;
40
+ }
41
+
42
+ if (run.backburner) {
43
+ return run.backburner;
44
+ }
45
+
46
+ return {
47
+ on() {},
48
+ off() {},
49
+ };
50
+ }
51
+
52
+ function getTransitionInformation(transition: any, router: any) {
53
+ const fromRoute = transition?.from?.name;
54
+ const toRoute = transition && transition.to ? transition.to.name : router.currentRouteName;
55
+ return {
56
+ fromRoute,
57
+ toRoute,
58
+ };
59
+ }
60
+
61
+ function getLocationURL(location: any) {
62
+ if (!location || !location.getURL || !location.formatURL) {
63
+ return '';
64
+ }
65
+ const url = location.formatURL(location.getURL());
66
+
67
+ if (location.implementation === 'hash') {
68
+ return `${location.rootURL}${url}`;
69
+ }
70
+ return url;
71
+ }
72
+
73
+ export function _instrumentEmberRouter(
74
+ routerService: any,
75
+ routerMain: any,
76
+ config: EmberSentryConfig,
77
+ startTransaction: Function,
78
+ startTransactionOnPageLoad?: boolean,
79
+ ) {
80
+ const { disableRunloopPerformance } = config;
81
+ const location = routerMain.location;
82
+ let activeTransaction: Transaction;
83
+ let transitionSpan: Span;
84
+
85
+ const url = getLocationURL(location);
86
+
87
+ if (macroCondition(isTesting())) {
88
+ routerService._sentryInstrumented = true;
89
+ routerService._startTransaction = startTransaction;
90
+ }
91
+
92
+ if (startTransactionOnPageLoad && url) {
93
+ const routeInfo = routerService.recognize(url);
94
+ activeTransaction = startTransaction({
95
+ name: `route:${routeInfo.name}`,
96
+ op: 'pageload',
97
+ tags: {
98
+ url,
99
+ toRoute: routeInfo.name,
100
+ 'routing.instrumentation': '@sentry/ember',
101
+ },
102
+ });
103
+ }
104
+
105
+ const finishActiveTransaction = function (_: any, nextInstance: any) {
106
+ if (nextInstance) {
107
+ return;
108
+ }
109
+ activeTransaction.finish();
110
+ getBackburner().off('end', finishActiveTransaction);
111
+ };
112
+
113
+ routerService.on('routeWillChange', (transition: any) => {
114
+ const { fromRoute, toRoute } = getTransitionInformation(transition, routerService);
115
+ activeTransaction?.finish();
116
+ activeTransaction = startTransaction({
117
+ name: `route:${toRoute}`,
118
+ op: 'navigation',
119
+ tags: {
120
+ fromRoute,
121
+ toRoute,
122
+ 'routing.instrumentation': '@sentry/ember',
123
+ },
124
+ });
125
+ transitionSpan = activeTransaction.startChild({
126
+ op: 'ui.ember.transition',
127
+ description: `route:${fromRoute} -> route:${toRoute}`,
128
+ });
129
+ });
130
+
131
+ routerService.on('routeDidChange', () => {
132
+ if (!transitionSpan || !activeTransaction) {
133
+ return;
134
+ }
135
+ transitionSpan.finish();
136
+
137
+ if (disableRunloopPerformance) {
138
+ activeTransaction.finish();
139
+ return;
140
+ }
141
+
142
+ getBackburner().on('end', finishActiveTransaction);
143
+ });
144
+
145
+ return {
146
+ startTransaction,
147
+ };
148
+ }
149
+
150
+ function _instrumentEmberRunloop(config: EmberSentryConfig) {
151
+ const { disableRunloopPerformance, minimumRunloopQueueDuration } = config;
152
+ if (disableRunloopPerformance) {
153
+ return;
154
+ }
155
+
156
+ let currentQueueStart: number | undefined;
157
+ let currentQueueSpan: Span | undefined;
158
+ const instrumentedEmberQueues = [
159
+ 'actions',
160
+ 'routerTransitions',
161
+ 'render',
162
+ 'afterRender',
163
+ 'destroy',
164
+ ] as EmberRunQueues[];
165
+
166
+ getBackburner().on('begin', (_: any, previousInstance: any) => {
167
+ if (previousInstance) {
168
+ return;
169
+ }
170
+ const activeTransaction = getActiveTransaction();
171
+ if (!activeTransaction) {
172
+ return;
173
+ }
174
+ if (currentQueueSpan) {
175
+ currentQueueSpan.finish();
176
+ }
177
+ currentQueueStart = timestampWithMs();
178
+
179
+ instrumentedEmberQueues.forEach(queue => {
180
+ scheduleOnce(queue, null, () => {
181
+ scheduleOnce(queue, null, () => {
182
+ // Process this queue using the end of the previous queue.
183
+ if (currentQueueStart) {
184
+ const now = timestampWithMs();
185
+ const minQueueDuration = minimumRunloopQueueDuration ?? 5;
186
+
187
+ if ((now - currentQueueStart) * 1000 >= minQueueDuration) {
188
+ activeTransaction
189
+ ?.startChild({
190
+ op: `ui.ember.runloop.${queue}`,
191
+ startTimestamp: currentQueueStart,
192
+ endTimestamp: now,
193
+ })
194
+ .finish();
195
+ }
196
+ currentQueueStart = undefined;
197
+ }
198
+
199
+ // Setup for next queue
200
+
201
+ const stillActiveTransaction = getActiveTransaction();
202
+ if (!stillActiveTransaction) {
203
+ return;
204
+ }
205
+ currentQueueStart = timestampWithMs();
206
+ });
207
+ });
208
+ });
209
+ });
210
+ getBackburner().on('end', (_: any, nextInstance: any) => {
211
+ if (nextInstance) {
212
+ return;
213
+ }
214
+ if (currentQueueSpan) {
215
+ currentQueueSpan.finish();
216
+ currentQueueSpan = undefined;
217
+ }
218
+ });
219
+ }
220
+
221
+ type Payload = {
222
+ containerKey: string;
223
+ initialRender: true;
224
+ object: string;
225
+ };
226
+
227
+ type RenderEntry = {
228
+ payload: Payload;
229
+ now: number;
230
+ };
231
+
232
+ interface RenderEntries {
233
+ [name: string]: RenderEntry;
234
+ }
235
+
236
+ function processComponentRenderBefore(payload: Payload, beforeEntries: RenderEntries) {
237
+ const info = {
238
+ payload,
239
+ now: timestampWithMs(),
240
+ };
241
+ beforeEntries[payload.object] = info;
242
+ }
243
+
244
+ function processComponentRenderAfter(
245
+ payload: Payload,
246
+ beforeEntries: RenderEntries,
247
+ op: string,
248
+ minComponentDuration: number,
249
+ ) {
250
+ const begin = beforeEntries[payload.object];
251
+
252
+ if (!begin) {
253
+ return;
254
+ }
255
+
256
+ const now = timestampWithMs();
257
+ const componentRenderDuration = now - begin.now;
258
+
259
+ if (componentRenderDuration * 1000 >= minComponentDuration) {
260
+ const activeTransaction = getActiveTransaction();
261
+
262
+ activeTransaction?.startChild({
263
+ op,
264
+ description: payload.containerKey || payload.object,
265
+ startTimestamp: begin.now,
266
+ endTimestamp: now,
267
+ });
268
+ }
269
+ }
270
+
271
+ function _instrumentComponents(config: EmberSentryConfig) {
272
+ const { disableInstrumentComponents, minimumComponentRenderDuration, enableComponentDefinitions } = config;
273
+ if (disableInstrumentComponents) {
274
+ return;
275
+ }
276
+
277
+ const minComponentDuration = minimumComponentRenderDuration ?? 2;
278
+
279
+ const beforeEntries = {} as RenderEntries;
280
+ const beforeComponentDefinitionEntries = {} as RenderEntries;
281
+
282
+ const subscribe = Ember.subscribe;
283
+ function _subscribeToRenderEvents() {
284
+ subscribe('render.component', {
285
+ before(_name: string, _timestamp: number, payload: Payload) {
286
+ processComponentRenderBefore(payload, beforeEntries);
287
+ },
288
+
289
+ after(_name: string, _timestamp: number, payload: any, _beganIndex: number) {
290
+ processComponentRenderAfter(payload, beforeEntries, 'ui.ember.component.render', minComponentDuration);
291
+ },
292
+ });
293
+ if (enableComponentDefinitions) {
294
+ subscribe('render.getComponentDefinition', {
295
+ before(_name: string, _timestamp: number, payload: Payload) {
296
+ processComponentRenderBefore(payload, beforeComponentDefinitionEntries);
297
+ },
298
+
299
+ after(_name: string, _timestamp: number, payload: any, _beganIndex: number) {
300
+ processComponentRenderAfter(payload, beforeComponentDefinitionEntries, 'ui.ember.component.definition', 0);
301
+ },
302
+ });
303
+ }
304
+ }
305
+ _subscribeToRenderEvents();
306
+ }
307
+
308
+ function _instrumentInitialLoad(config: EmberSentryConfig) {
309
+ const startName = '@sentry/ember:initial-load-start';
310
+ const endName = '@sentry/ember:initial-load-end';
311
+
312
+ const { performance } = window;
313
+ const HAS_PERFORMANCE = performance && performance.clearMarks && performance.clearMeasures;
314
+
315
+ if (!HAS_PERFORMANCE) {
316
+ return;
317
+ }
318
+
319
+ if (config.disableInitialLoadInstrumentation) {
320
+ performance.clearMarks(startName);
321
+ performance.clearMarks(endName);
322
+ return;
323
+ }
324
+
325
+ // Split performance check in two so clearMarks still happens even if timeOrigin isn't available.
326
+ const HAS_PERFORMANCE_TIMING =
327
+ performance.measure && performance.getEntriesByName && browserPerformanceTimeOrigin !== undefined;
328
+ if (!HAS_PERFORMANCE_TIMING) {
329
+ return;
330
+ }
331
+ const measureName = '@sentry/ember:initial-load';
332
+
333
+ const startMarkExists = performance.getEntriesByName(startName).length > 0;
334
+ const endMarkExists = performance.getEntriesByName(endName).length > 0;
335
+ if (!startMarkExists || !endMarkExists) {
336
+ return;
337
+ }
338
+
339
+ performance.measure(measureName, startName, endName);
340
+ const measures = performance.getEntriesByName(measureName);
341
+ const measure = measures[0];
342
+
343
+ const startTimestamp = (measure.startTime + browserPerformanceTimeOrigin!) / 1000;
344
+ const endTimestamp = startTimestamp + measure.duration / 1000;
345
+
346
+ const transaction = getActiveTransaction();
347
+ const span = transaction?.startChild({
348
+ op: 'ui.ember.init',
349
+ startTimestamp,
350
+ });
351
+ span?.finish(endTimestamp);
352
+ performance.clearMarks(startName);
353
+ performance.clearMarks(endName);
354
+
355
+ performance.clearMeasures(measureName);
356
+ }
357
+
358
+ export async function instrumentForPerformance(appInstance: ApplicationInstance) {
359
+ const config = getSentryConfig();
360
+ const sentryConfig = config.sentry;
361
+ // Maintaining backwards compatibility with config.browserTracingOptions, but passing it with Sentry options is preferred.
362
+ const browserTracingOptions = config.browserTracingOptions || config.sentry.browserTracingOptions || {};
363
+
364
+ const tracing = await import('@sentry/tracing');
365
+
366
+ const idleTimeout = config.transitionTimeout || 5000;
367
+
368
+ const existingIntegrations = (sentryConfig['integrations'] || []) as Integration[];
369
+
370
+ sentryConfig['integrations'] = [
371
+ ...existingIntegrations,
372
+ new tracing.Integrations.BrowserTracing({
373
+ routingInstrumentation: (customStartTransaction, startTransactionOnPageLoad) => {
374
+ const routerMain = appInstance.lookup('router:main');
375
+ const routerService = appInstance.lookup('service:router');
376
+ _instrumentEmberRouter(routerService, routerMain, config, customStartTransaction, startTransactionOnPageLoad);
377
+ },
378
+ idleTimeout,
379
+ ...browserTracingOptions,
380
+ }),
381
+ ];
382
+
383
+ class FakeBrowserTracingClass {
384
+ static id = tracing.BROWSER_TRACING_INTEGRATION_ID;
385
+ public name = FakeBrowserTracingClass.id;
386
+ setupOnce() {
387
+ // noop - We're just faking this class for a lookup
388
+ }
389
+ }
390
+
391
+ if (
392
+ isTesting() &&
393
+ Sentry.getCurrentHub()?.getIntegration(
394
+ // This is a temporary hack because the BrowserTracing integration cannot have a static `id` field for tree
395
+ // shaking reasons. However, `getIntegration` needs that field.
396
+ FakeBrowserTracingClass,
397
+ )
398
+ ) {
399
+ // Initializers are called more than once in tests, causing the integrations to not be setup correctly.
400
+ return;
401
+ }
402
+
403
+ Sentry.init(sentryConfig); // Call init again to rebind client with new integration list in addition to the defaults
404
+
405
+ _instrumentEmberRunloop(config);
406
+ _instrumentComponents(config);
407
+ _instrumentInitialLoad(config);
408
+ }
409
+
410
+ export default {
411
+ initialize,
412
+ };
@@ -0,0 +1,19 @@
1
+ import { Backburner } from '@ember/runloop/-private/backburner';
2
+
3
+ /**
4
+ * Backburner needs to be extended as it's missing the 'off' method.
5
+ */
6
+ interface ExtendedBackburner extends Backburner {
7
+ off(...args: any[]): void;
8
+ }
9
+
10
+ /**
11
+ * Runloop needs to be extended to expose backburner as suggested here:
12
+ * https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/ember__runloop/ember__runloop-tests.ts#L9
13
+ */
14
+ declare module '@ember/runloop' {
15
+ interface RunNamespace {
16
+ backburner?: ExtendedBackburner;
17
+ }
18
+ export const _backburner: ExtendedBackburner; // Ember 4.0
19
+ }
package/addon/types.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { BrowserOptions } from '@sentry/browser';
2
+
3
+ export type EmberSentryConfig = {
4
+ sentry: BrowserOptions & { browserTracingOptions: Object };
5
+ transitionTimeout: number;
6
+ ignoreEmberOnErrorWarning: boolean;
7
+ disableInstrumentComponents: boolean;
8
+ disablePerformance: boolean;
9
+ disablePostTransitionRender: boolean;
10
+ disableRunloopPerformance: boolean;
11
+ disableInitialLoadInstrumentation: boolean;
12
+ enableComponentDefinitions: boolean;
13
+ minimumRunloopQueueDuration: number;
14
+ minimumComponentRenderDuration: number;
15
+ browserTracingOptions: Object;
16
+ };
17
+
18
+ export type OwnConfig = {
19
+ sentryConfig: EmberSentryConfig;
20
+ };
21
+
22
+ export type GlobalConfig = {
23
+ __sentryEmberConfig: EmberSentryConfig['sentry'];
24
+ };
@@ -0,0 +1 @@
1
+ export { default, initialize } from '@sentry/ember/instance-initializers/sentry-performance';
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ module.exports = function (/* environment, appConfig */) {
4
+ return {};
5
+ };
package/ember.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import Ember from 'ember';
2
+ declare module 'ember' {
3
+ namespace Ember {
4
+ export function subscribe(pattern: string, object: {}): any;
5
+ }
6
+ }
package/index.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { BrowserOptions } from '@sentry/browser';
2
+ export declare function InitSentryForEmber(_runtimeConfig?: BrowserOptions): void;
3
+ export declare const getActiveTransaction: () => import("@sentry/types").Transaction | undefined;
4
+ export declare const instrumentRoutePerformance: (BaseRoute: any) => {
5
+ new (): {
6
+ [x: string]: any;
7
+ beforeModel(...args: any[]): Promise<any>;
8
+ model(...args: any[]): Promise<any>;
9
+ afterModel(...args: any[]): Promise<any>;
10
+ setupController(...args: any[]): Promise<any>;
11
+ };
12
+ [x: string]: any;
13
+ };
14
+ export * from '@sentry/browser';
15
+ export declare const init: typeof InitSentryForEmber;
package/index.js ADDED
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const crypto = require('crypto');
4
+
5
+ function readSnippet(fileName) {
6
+ return fs.readFileSync(`${__dirname}/vendor/${fileName}`, 'utf8');
7
+ }
8
+
9
+ function hashSha256base64(string) {
10
+ return crypto.createHash('sha256').update(string).digest('base64');
11
+ }
12
+
13
+ const initialLoadHeadSnippet = readSnippet('initial-load-head.js');
14
+ const initialLoadBodySnippet = readSnippet('initial-load-body.js');
15
+
16
+ const initialLoadHeadSnippetHash = hashSha256base64(initialLoadHeadSnippet);
17
+ const initialLoadBodySnippetHash = hashSha256base64(initialLoadBodySnippet);
18
+
19
+ module.exports = {
20
+ name: require('./package').name,
21
+ options: {
22
+ babel: {
23
+ plugins: [require.resolve('ember-auto-import/babel-plugin')],
24
+ },
25
+ '@embroider/macros': {
26
+ setOwnConfig: {},
27
+ },
28
+ },
29
+
30
+ config(_, appConfig) {
31
+ const addonConfig = appConfig['@sentry/ember'];
32
+ this.options['@embroider/macros'].setOwnConfig.sentryConfig = { ...addonConfig };
33
+ return this._super(...arguments);
34
+ },
35
+
36
+ contentFor(type, config) {
37
+ const addonConfig = config['@sentry/ember'] || {};
38
+ const { disablePerformance, disableInitialLoadInstrumentation } = addonConfig;
39
+
40
+ if (disablePerformance || disableInitialLoadInstrumentation) {
41
+ return;
42
+ }
43
+
44
+ if (type === 'head') {
45
+ return `<script type="text/javascript">${initialLoadHeadSnippet}</script>`;
46
+ } else if (type === 'body-footer') {
47
+ return `<script type="text/javascript">${initialLoadBodySnippet}</script>`;
48
+ }
49
+ },
50
+
51
+ injectedScriptHashes: [initialLoadHeadSnippetHash, initialLoadBodySnippetHash],
52
+ };
@@ -0,0 +1,11 @@
1
+ import ApplicationInstance from '@ember/application/instance';
2
+ import { EmberSentryConfig } from '../types';
3
+ export declare function initialize(appInstance: ApplicationInstance): void;
4
+ export declare function _instrumentEmberRouter(routerService: any, routerMain: any, config: EmberSentryConfig, startTransaction: Function, startTransactionOnPageLoad?: boolean): {
5
+ startTransaction: Function;
6
+ };
7
+ export declare function instrumentForPerformance(appInstance: ApplicationInstance): Promise<void>;
8
+ declare const _default: {
9
+ initialize: typeof initialize;
10
+ };
11
+ export default _default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/ember",
3
- "version": "7.3.0",
3
+ "version": "7.4.1",
4
4
  "description": "Official Sentry SDK for Ember.js",
5
5
  "repository": "git://github.com/getsentry/sentry-javascript.git",
6
6
  "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/ember",
@@ -30,10 +30,10 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@embroider/macros": "~0.47.2",
33
- "@sentry/browser": "7.3.0",
34
- "@sentry/tracing": "7.3.0",
35
- "@sentry/types": "7.3.0",
36
- "@sentry/utils": "7.3.0",
33
+ "@sentry/browser": "7.4.1",
34
+ "@sentry/tracing": "7.4.1",
35
+ "@sentry/types": "7.4.1",
36
+ "@sentry/utils": "7.4.1",
37
37
  "ember-auto-import": "~1.12.1 || ~2.2.0",
38
38
  "ember-cli-babel": "~7.26.6",
39
39
  "ember-cli-htmlbars": "^6.0.1",
package/runloop.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { Backburner } from '@ember/runloop/-private/backburner';
2
+
3
+ /**
4
+ * Backburner needs to be extended as it's missing the 'off' method.
5
+ */
6
+ interface ExtendedBackburner extends Backburner {
7
+ off(...args: any[]): void;
8
+ }
9
+
10
+ /**
11
+ * Runloop needs to be extended to expose backburner as suggested here:
12
+ * https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/ember__runloop/ember__runloop-tests.ts#L9
13
+ */
14
+ declare module '@ember/runloop' {
15
+ interface RunNamespace {
16
+ backburner?: ExtendedBackburner;
17
+ }
18
+ export const _backburner: ExtendedBackburner; // Ember 4.0
19
+ }
@@ -0,0 +1,17 @@
1
+ /*eslint-env node*/
2
+ const { spawnSync } = require('child_process');
3
+
4
+ console.log('Mimicking the CI environment in order to run tests against multiple versions of Ember');
5
+
6
+ const result = spawnSync('yarn test', {
7
+ shell: true,
8
+ stdio: 'inherit',
9
+ env: {
10
+ ...process.env,
11
+ GITHUB_ACTIONS: true,
12
+ GITHUB_EVENT_NAME: 'push',
13
+ GITHUB_HEAD_REF: 'master',
14
+ },
15
+ });
16
+
17
+ process.exit(result.status);
package/tsconfig.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "target": "es2017",
5
+ "allowJs": true,
6
+ "moduleResolution": "node",
7
+ "allowSyntheticDefaultImports": true,
8
+ "alwaysStrict": true,
9
+ "noImplicitUseStrict": false,
10
+ "strictNullChecks": true,
11
+ "strictPropertyInitialization": true,
12
+ "noEmitOnError": false,
13
+ "noEmit": true,
14
+ "baseUrl": ".",
15
+ "module": "esnext",
16
+ "experimentalDecorators": true,
17
+ "paths": {
18
+ "dummy/tests/*": ["tests/*"],
19
+ "dummy/*": ["tests/dummy/app/*", "app/*"],
20
+ "@sentry/ember": ["addon"],
21
+ "@sentry/ember/*": ["addon/*"],
22
+ "@sentry/ember/test-support": ["addon-test-support"],
23
+ "@sentry/ember/test-support/*": ["addon-test-support/*"],
24
+ "*": ["types/*"]
25
+ }
26
+ },
27
+ "include": ["app/**/*", "addon/**/*", "types/**/*", "addon-test-support/**/*"]
28
+ }
package/types.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ import { BrowserOptions } from '@sentry/browser';
2
+ export declare type EmberSentryConfig = {
3
+ sentry: BrowserOptions & {
4
+ browserTracingOptions: Object;
5
+ };
6
+ transitionTimeout: number;
7
+ ignoreEmberOnErrorWarning: boolean;
8
+ disableInstrumentComponents: boolean;
9
+ disablePerformance: boolean;
10
+ disablePostTransitionRender: boolean;
11
+ disableRunloopPerformance: boolean;
12
+ disableInitialLoadInstrumentation: boolean;
13
+ enableComponentDefinitions: boolean;
14
+ minimumRunloopQueueDuration: number;
15
+ minimumComponentRenderDuration: number;
16
+ browserTracingOptions: Object;
17
+ };
18
+ export declare type OwnConfig = {
19
+ sentryConfig: EmberSentryConfig;
20
+ };
21
+ export declare type GlobalConfig = {
22
+ __sentryEmberConfig: EmberSentryConfig['sentry'];
23
+ };
@@ -0,0 +1,3 @@
1
+ if (window.performance && window.performance.mark) {
2
+ window.performance.mark('@sentry/ember:initial-load-end');
3
+ }
@@ -0,0 +1,3 @@
1
+ if (window.performance && window.performance.mark) {
2
+ window.performance.mark('@sentry/ember:initial-load-start');
3
+ }