Skip to content

feat(ember): Add more render instrumentation to @sentry/ember #2902

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Sep 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased

- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
- [ember] feat: Add more render instrumentation (#2902)

## 5.24.2

Expand Down
53 changes: 47 additions & 6 deletions packages/ember/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Aside from configuration passed from this addon into `@sentry/browser` via the `
sentry: ... // See sentry-javascript configuration https://docs.sentry.io/error-reporting/configuration/?platform=javascript
};
```

#### Disabling Performance

`@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`:
Expand All @@ -82,13 +83,53 @@ you can import `instrumentRoutePerformance` and wrap your route with it.
import Route from '@ember/routing/route';
import { instrumentRoutePerformance } from '@sentry/ember';

export default instrumentRoutePerformance(
class MyRoute extends Route {
model() {
//...
}
class MyRoute extends Route {
model() {
//...
}
);
}

export default instrumentRoutePerformance(MyRoute);
```

#### Runloop
The runloop queue durations are instrumented by default, as long as they are longer than a threshold (by default 5ms).
This helps (via the render queue) capturing the entire render in case component render times aren't fully instrumented,
such as when using glimmer components.

If you would like to change the runloop queue threshold, add the following to your config:
```javascript
ENV['@sentry/ember'] = {
minimumRunloopQueueDuration: 0, // All runloop queue durations will be added as spans.
};
```

#### Components
Non-glimmer component render times will automatically get captured.

If you would like to disable component render being instrumented, add the following to your config:
```javascript
ENV['@sentry/ember'] = {
disableInstrumentComponents: true, // Will disable automatic instrumentation for components.
};
```

Additionally, components whose render time is below a threshold (by default 2ms) will not be included as spans.
If you would like to change this threshold, add the following to your config:
```javascript
ENV['@sentry/ember'] = {
minimumComponentRenderDuration: 0, // All (non-glimmer) component render durations will be added as spans.
};
```

#### Glimmer components
Currently glimmer component render durations can only be captured indirectly via the runloop instrumentation. You can
optionally enable a setting to show component definitions (which will indicate which components are being rendered) be
adding the following to your config:
```javascript
ENV['@sentry/ember'] = {
enableComponentDefinition: true, // All component definitions will be added as spans.
};
```

### Supported Versions
Expand Down
6 changes: 6 additions & 0 deletions packages/ember/addon/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@ declare module 'ember-get-config' {
sentry: BrowserOptions;
transitionTimeout: number;
ignoreEmberOnErrorWarning: boolean;
disableInstrumentComponents: boolean;
disablePerformance: boolean;
disablePostTransitionRender: boolean;
disableRunloopPerformance: boolean;
disableInitialLoadInstrumentation: boolean;
enableComponentDefinitions: boolean;
minimumRunloopQueueDuration: number;
minimumComponentRenderDuration: number;
};
const config: {
'@sentry/ember': EmberSentryConfig;
Expand Down
6 changes: 6 additions & 0 deletions packages/ember/addon/ember.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Ember from 'ember';
declare module 'ember' {
namespace Ember {
export function subscribe(pattern: string, object: {}): any;
}
}
58 changes: 35 additions & 23 deletions packages/ember/addon/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import environmentConfig from 'ember-get-config';
import { next } from '@ember/runloop';
import { assert, warn, runInDebug } from '@ember/debug';
import Ember from 'ember';
import { timestampWithMs } from '@sentry/utils';

declare module '@ember/debug' {
export function assert(desc: string, test: unknown): void;
Expand Down Expand Up @@ -37,38 +38,49 @@ export function InitSentryForEmber(_runtimeConfig: BrowserOptions | undefined) {
});
}

const getCurrentTransaction = () => {
export const getActiveTransaction = () => {
return Sentry.getCurrentHub()
?.getScope()
?.getTransaction();
};

const instrumentFunction = async (op: string, description: string, fn: Function, args: any) => {
const currentTransaction = getCurrentTransaction();
const span = currentTransaction?.startChild({ op, description });
const result = await fn(...args);
span?.finish();
return result;
};

export const instrumentRoutePerformance = (BaseRoute: any) => {
return class InstrumentedRoute extends BaseRoute {
beforeModel(...args: any[]) {
return instrumentFunction('ember.route.beforeModel', (<any>this).fullRouteName, super.beforeModel, args);
}

async model(...args: any[]) {
return instrumentFunction('ember.route.model', (<any>this).fullRouteName, super.model, args);
}

async afterModel(...args: any[]) {
return instrumentFunction('ember.route.afterModel', (<any>this).fullRouteName, super.afterModel, args);
}
const instrumentFunction = async (op: string, description: string, fn: Function, args: any) => {
const startTimestamp = timestampWithMs();
const result = await fn(...args);

async setupController(...args: any[]) {
return instrumentFunction('ember.route.setupController', (<any>this).fullRouteName, super.setupController, args);
const currentTransaction = getActiveTransaction();
if (!currentTransaction) {
return result;
}
currentTransaction.startChild({ op, description, startTimestamp }).finish();
return result;
};

return {
[BaseRoute.name]: class extends BaseRoute {
beforeModel(...args: any[]) {
return instrumentFunction('ember.route.beforeModel', (<any>this).fullRouteName, super.beforeModel, args);
}

async model(...args: any[]) {
return instrumentFunction('ember.route.model', (<any>this).fullRouteName, super.model, args);
}

async afterModel(...args: any[]) {
return instrumentFunction('ember.route.afterModel', (<any>this).fullRouteName, super.afterModel, args);
}

async setupController(...args: any[]) {
return instrumentFunction(
'ember.route.setupController',
(<any>this).fullRouteName,
super.setupController,
args,
);
}
},
}[BaseRoute.name];
};

function createEmberEventProcessor(): void {
Expand Down
Loading