Skip to content

Commit 2e761fe

Browse files
authored
feat: Add basic router instrumentation for @sentry/ember (#2784)
* feat: Add router instrumentation for @sentry/ember This will add routing instrumentation for Ember transitions to @sentry/ember. The import will be dynamically loading depending on configuration settings with to not increase bundle size. * Run ember tests in github actions * Re-introduce ember into scripts/test * Add render runloop post transition as a child span * Add basic transaction tests to ensure router instrumentation works properly. Split off router instrumentation to allow for use in testing
1 parent d315f0f commit 2e761fe

32 files changed

+587
-143
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ jobs:
8787
key: ${{ runner.os }}-${{ github.sha }}
8888
- run: yarn install
8989
- name: Unit Tests
90-
run: yarn test --ignore="@sentry/ember"
90+
run: yarn test
9191
- uses: codecov/codecov-action@v1
9292

9393
job_browserstack_test:

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Unreleased
44

55
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
6+
- [ember] feat: Add performance instrumentation for routes (#2784)
67

78
## 5.22.3
89

packages/ember/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,34 @@ Aside from configuration passed from this addon into `@sentry/browser` via the `
6262
sentry: ... // See sentry-javascript configuration https://docs.sentry.io/error-reporting/configuration/?platform=javascript
6363
};
6464
```
65+
#### Disabling Performance
66+
67+
`@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`:
68+
69+
```javascript
70+
ENV['@sentry/ember'] = {
71+
disablePerformance: true, // Will disable automatic instrumentation of performance. Manual instrumentation will still be sent.
72+
sentry: ... // See sentry-javascript configuration https://docs.sentry.io/error-reporting/configuration/?platform=javascript
73+
};
74+
```
75+
76+
### Performance
77+
#### Routes
78+
If you would like to capture `beforeModel`, `model`, `afterModel` and `setupController` times for one of your routes,
79+
you can import `instrumentRoutePerformance` and wrap your route with it.
80+
81+
```javascript
82+
import Route from '@ember/routing/route';
83+
import { instrumentRoutePerformance } from '@sentry/ember';
84+
85+
export default instrumentRoutePerformance(
86+
class MyRoute extends Route {
87+
model() {
88+
//...
89+
}
90+
}
91+
);
92+
```
6593

6694
### Supported Versions
6795

packages/ember/addon/config.d.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
declare module 'ember-get-config' {
2+
import { BrowserOptions } from '@sentry/browser';
3+
type EmberSentryConfig = {
4+
sentry: BrowserOptions;
5+
transitionTimeout: number;
6+
ignoreEmberOnErrorWarning: boolean;
7+
disablePerformance: boolean;
8+
disablePostTransitionRender: boolean;
9+
};
10+
const config: {
11+
'@sentry/ember': EmberSentryConfig;
12+
};
13+
export default config;
14+
}

packages/ember/addon/declarations.d.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.

packages/ember/addon/index.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,40 @@ export function InitSentryForEmber(_runtimeConfig: BrowserOptions | undefined) {
3737
});
3838
}
3939

40+
const getCurrentTransaction = () => {
41+
return Sentry.getCurrentHub()
42+
?.getScope()
43+
?.getTransaction();
44+
};
45+
46+
const instrumentFunction = async (op: string, description: string, fn: Function, args: any) => {
47+
const currentTransaction = getCurrentTransaction();
48+
const span = currentTransaction?.startChild({ op, description });
49+
const result = await fn(...args);
50+
span?.finish();
51+
return result;
52+
};
53+
54+
export const instrumentRoutePerformance = (BaseRoute: any) => {
55+
return class InstrumentedRoute extends BaseRoute {
56+
beforeModel(...args: any[]) {
57+
return instrumentFunction('ember.route.beforeModel', (<any>this).fullRouteName, super.beforeModel, args);
58+
}
59+
60+
async model(...args: any[]) {
61+
return instrumentFunction('ember.route.model', (<any>this).fullRouteName, super.model, args);
62+
}
63+
64+
async afterModel(...args: any[]) {
65+
return instrumentFunction('ember.route.afterModel', (<any>this).fullRouteName, super.afterModel, args);
66+
}
67+
68+
async setupController(...args: any[]) {
69+
return instrumentFunction('ember.route.setupController', (<any>this).fullRouteName, super.setupController, args);
70+
}
71+
};
72+
};
73+
4074
function createEmberEventProcessor(): void {
4175
if (addGlobalEventProcessor) {
4276
addGlobalEventProcessor(event => {
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import ApplicationInstance from '@ember/application/instance';
2+
import Ember from 'ember';
3+
import { scheduleOnce } from '@ember/runloop';
4+
import environmentConfig from 'ember-get-config';
5+
import Sentry from '@sentry/browser';
6+
import { Integration } from '@sentry/types';
7+
8+
export function initialize(appInstance: ApplicationInstance): void {
9+
const config = environmentConfig['@sentry/ember'];
10+
if (config['disablePerformance']) {
11+
return;
12+
}
13+
const performancePromise = instrumentForPerformance(appInstance);
14+
if (Ember.testing) {
15+
(<any>window)._sentryPerformanceLoad = performancePromise;
16+
}
17+
}
18+
19+
function getTransitionInformation(transition: any, router: any) {
20+
const fromRoute = transition?.from?.name;
21+
const toRoute = transition ? transition.to.name : router.currentRouteName;
22+
return {
23+
fromRoute,
24+
toRoute,
25+
};
26+
}
27+
28+
export function _instrumentEmberRouter(
29+
routerService: any,
30+
routerMain: any,
31+
config: typeof environmentConfig['@sentry/ember'],
32+
startTransaction: Function,
33+
startTransactionOnPageLoad?: boolean,
34+
) {
35+
const { disablePostTransitionRender } = config;
36+
const location = routerMain.location;
37+
let activeTransaction: any;
38+
let transitionSpan: any;
39+
40+
const url = location && location.getURL && location.getURL();
41+
42+
if (Ember.testing) {
43+
routerService._sentryInstrumented = true;
44+
}
45+
46+
if (startTransactionOnPageLoad && url) {
47+
const routeInfo = routerService.recognize(url);
48+
activeTransaction = startTransaction({
49+
name: `route:${routeInfo.name}`,
50+
op: 'pageload',
51+
tags: {
52+
url,
53+
toRoute: routeInfo.name,
54+
'routing.instrumentation': '@sentry/ember',
55+
},
56+
});
57+
}
58+
59+
routerService.on('routeWillChange', (transition: any) => {
60+
const { fromRoute, toRoute } = getTransitionInformation(transition, routerService);
61+
activeTransaction = startTransaction({
62+
name: `route:${toRoute}`,
63+
op: 'navigation',
64+
tags: {
65+
fromRoute,
66+
toRoute,
67+
'routing.instrumentation': '@sentry/ember',
68+
},
69+
});
70+
transitionSpan = activeTransaction.startChild({
71+
op: 'ember.transition',
72+
description: `route:${fromRoute} -> route:${toRoute}`,
73+
});
74+
});
75+
76+
routerService.on('routeDidChange', (transition: any) => {
77+
const { toRoute } = getTransitionInformation(transition, routerService);
78+
let renderSpan: any;
79+
if (!transitionSpan || !activeTransaction) {
80+
return;
81+
}
82+
transitionSpan.finish();
83+
84+
if (disablePostTransitionRender) {
85+
activeTransaction.finish();
86+
}
87+
88+
function startRenderSpan() {
89+
renderSpan = activeTransaction.startChild({
90+
op: 'ember.runloop.render',
91+
description: `post-transition render route:${toRoute}`,
92+
});
93+
}
94+
95+
function finishRenderSpan() {
96+
renderSpan.finish();
97+
activeTransaction.finish();
98+
}
99+
100+
scheduleOnce('routerTransitions', null, startRenderSpan);
101+
scheduleOnce('afterRender', null, finishRenderSpan);
102+
});
103+
}
104+
105+
export async function instrumentForPerformance(appInstance: ApplicationInstance) {
106+
const config = environmentConfig['@sentry/ember'];
107+
const sentryConfig = config.sentry;
108+
const tracing = await import('@sentry/tracing');
109+
110+
const idleTimeout = config.transitionTimeout || 5000;
111+
112+
const existingIntegrations = (sentryConfig['integrations'] || []) as Integration[];
113+
114+
sentryConfig['integrations'] = [
115+
...existingIntegrations,
116+
new tracing.Integrations.BrowserTracing({
117+
routingInstrumentation: (startTransaction, startTransactionOnPageLoad) => {
118+
const routerMain = appInstance.lookup('router:main');
119+
const routerService = appInstance.lookup('service:router');
120+
_instrumentEmberRouter(routerService, routerMain, config, startTransaction, startTransactionOnPageLoad);
121+
},
122+
idleTimeout,
123+
}),
124+
];
125+
126+
Sentry.init(sentryConfig); // Call init again to rebind client with new integration list in addition to the defaults
127+
}
128+
129+
export default {
130+
initialize,
131+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default, initialize } from '@sentry/ember/instance-initializers/sentry-performance';

packages/ember/index.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
'use strict';
22

33
module.exports = {
4-
name: require('./package').name
4+
name: require('./package').name,
5+
options: {
6+
babel: {
7+
plugins: [ require.resolve('ember-auto-import/babel-plugin') ]
8+
}
9+
}
510
};

packages/ember/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
},
3232
"dependencies": {
3333
"@sentry/browser": "5.22.3",
34+
"@sentry/tracing": "5.22.3",
3435
"@sentry/types": "5.22.3",
3536
"@sentry/utils": "5.22.3",
3637
"ember-auto-import": "^1.6.0",
@@ -69,6 +70,7 @@
6970
"ember-template-lint": "^2.9.1",
7071
"ember-test-selectors": "^4.1.0",
7172
"ember-try": "^1.4.0",
73+
"ember-window-mock": "^0.7.1",
7274
"eslint": "7.6.0",
7375
"eslint-plugin-ember": "^8.6.0",
7476
"eslint-plugin-node": "^11.1.0",

0 commit comments

Comments
 (0)