Skip to content

Commit 820bd77

Browse files
authored
Feat: Add @sentry/tracing (#2719)
* feat: Create IdleTransaction class (#2720) * feat: Add BrowserTracing integration (#2723) * feat: Add span creators to @sentry/tracing package (#2736) * ref: Convert React and Vue Tracing to use active transaction (#2741) * build: generate tracing bundles * fix: Remove circular dependency between span and transaction * ref: Add side effects true to tracing * build: Only include @sentry/browser for bundle * fix: Make sure vue and react are backwards compatible with @sentry/apm
1 parent 0f07871 commit 820bd77

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+3919
-121
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
## Unreleased
44

55
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
6+
- [react] feat: Export `createReduxEnhancer` to log redux actions as breadcrumbs, and attach state as an extra. (#2717)
7+
- [tracing] feat: `Add @sentry/tracing` (#2719)
68

79
## 5.19.2
810

@@ -17,7 +19,6 @@
1719
- [tracing] fix: APM CDN bundle expose startTransaction (#2726)
1820
- [browser] fix: Correctly remove all event listeners (#2725)
1921
- [tracing] fix: Add manual `DOMStringList` typing (#2718)
20-
- [react] feat: Export `createReduxEnhancer` to log redux actions as breadcrumbs, and attach state as an extra. (#2717)
2122

2223
## 5.19.0
2324

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"packages/minimal",
2929
"packages/node",
3030
"packages/react",
31+
"packages/tracing",
3132
"packages/types",
3233
"packages/typescript",
3334
"packages/utils"

packages/integrations/src/vue.ts

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
1-
import { EventProcessor, Hub, Integration, IntegrationClass, Span } from '@sentry/types';
1+
import { EventProcessor, Hub, Integration, IntegrationClass, Scope, Span, Transaction } from '@sentry/types';
22
import { basename, getGlobalObject, logger, timestampWithMs } from '@sentry/utils';
33

44
/**
55
* Used to extract Tracing integration from the current client,
66
* without the need to import `Tracing` itself from the @sentry/apm package.
7+
* @deprecated as @sentry/tracing should be used over @sentry/apm.
78
*/
89
const TRACING_GETTER = ({
910
id: 'Tracing',
1011
} as any) as IntegrationClass<Integration>;
1112

13+
/**
14+
* Used to extract BrowserTracing integration from @sentry/tracing
15+
*/
16+
const BROWSER_TRACING_GETTER = ({
17+
id: 'BrowserTracing',
18+
} as any) as IntegrationClass<Integration>;
19+
1220
/** Global Vue object limited to the methods/attributes we require */
1321
interface VueInstance {
1422
config: {
@@ -229,6 +237,7 @@ export class Vue implements Integration {
229237

230238
// We do this whole dance with `TRACING_GETTER` to prevent `@sentry/apm` from becoming a peerDependency.
231239
// We also need to ask for the `.constructor`, as `pushActivity` and `popActivity` are static, not instance methods.
240+
// tslint:disable-next-line: deprecation
232241
const tracingIntegration = getCurrentHub().getIntegration(TRACING_GETTER);
233242
if (tracingIntegration) {
234243
// tslint:disable-next-line:no-unsafe-any
@@ -242,6 +251,15 @@ export class Vue implements Integration {
242251
op: 'Vue',
243252
});
244253
}
254+
// Use functionality from @sentry/tracing
255+
} else {
256+
const activeTransaction = getActiveTransaction(getCurrentHub());
257+
if (activeTransaction) {
258+
this._rootSpan = activeTransaction.startChild({
259+
description: 'Application Render',
260+
op: 'Vue',
261+
});
262+
}
245263
}
246264
});
247265
}
@@ -315,15 +333,18 @@ export class Vue implements Integration {
315333
if (this._tracingActivity) {
316334
// We do this whole dance with `TRACING_GETTER` to prevent `@sentry/apm` from becoming a peerDependency.
317335
// We also need to ask for the `.constructor`, as `pushActivity` and `popActivity` are static, not instance methods.
336+
// tslint:disable-next-line: deprecation
318337
const tracingIntegration = getCurrentHub().getIntegration(TRACING_GETTER);
319338
if (tracingIntegration) {
320339
// tslint:disable-next-line:no-unsafe-any
321340
(tracingIntegration as any).constructor.popActivity(this._tracingActivity);
322-
if (this._rootSpan) {
323-
this._rootSpan.finish(timestamp);
324-
}
325341
}
326342
}
343+
344+
// We should always finish the span, only should pop activity if using @sentry/apm
345+
if (this._rootSpan) {
346+
this._rootSpan.finish(timestamp);
347+
}
327348
}, this._options.tracingOptions.timeout);
328349
}
329350

@@ -333,7 +354,8 @@ export class Vue implements Integration {
333354

334355
this._options.Vue.mixin({
335356
beforeCreate(this: ViewModel): void {
336-
if (getCurrentHub().getIntegration(TRACING_GETTER)) {
357+
// tslint:disable-next-line: deprecation
358+
if (getCurrentHub().getIntegration(TRACING_GETTER) || getCurrentHub().getIntegration(BROWSER_TRACING_GETTER)) {
337359
// `this` points to currently rendered component
338360
applyTracingHooks(this, getCurrentHub);
339361
} else {
@@ -405,3 +427,21 @@ export class Vue implements Integration {
405427
}
406428
}
407429
}
430+
431+
// tslint:disable-next-line: completed-docs
432+
interface HubType extends Hub {
433+
// tslint:disable-next-line: completed-docs
434+
getScope?(): Scope | undefined;
435+
}
436+
437+
/** Grabs active transaction off scope */
438+
export function getActiveTransaction<T extends Transaction>(hub: HubType): T | undefined {
439+
if (hub && hub.getScope) {
440+
const scope = hub.getScope() as Scope;
441+
if (scope) {
442+
return scope.getTransaction() as T | undefined;
443+
}
444+
}
445+
446+
return undefined;
447+
}

packages/react/src/profiler.tsx

Lines changed: 58 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { getCurrentHub } from '@sentry/browser';
2-
import { Integration, IntegrationClass, Span } from '@sentry/types';
3-
import { logger, timestampWithMs } from '@sentry/utils';
1+
import { getCurrentHub, Hub } from '@sentry/browser';
2+
import { Integration, IntegrationClass, Span, Transaction } from '@sentry/types';
3+
import { timestampWithMs } from '@sentry/utils';
44
import * as hoistNonReactStatic from 'hoist-non-react-statics';
55
import * as React from 'react';
66

@@ -11,6 +11,7 @@ const TRACING_GETTER = ({
1111
} as any) as IntegrationClass<Integration>;
1212

1313
let globalTracingIntegration: Integration | null = null;
14+
/** @deprecated remove when @sentry/apm no longer used */
1415
const getTracingIntegration = () => {
1516
if (globalTracingIntegration) {
1617
return globalTracingIntegration;
@@ -20,21 +21,11 @@ const getTracingIntegration = () => {
2021
return globalTracingIntegration;
2122
};
2223

23-
/**
24-
* Warn if tracing integration not configured. Will only warn once.
25-
*/
26-
function warnAboutTracing(name: string): void {
27-
if (globalTracingIntegration === null) {
28-
logger.warn(
29-
`Unable to profile component ${name} due to invalid Tracing Integration. Please make sure the Tracing integration is setup properly.`,
30-
);
31-
}
32-
}
33-
3424
/**
3525
* pushActivity creates an new react activity.
3626
* Is a no-op if Tracing integration is not valid
3727
* @param name displayName of component that started activity
28+
* @deprecated remove when @sentry/apm no longer used
3829
*/
3930
function pushActivity(name: string, op: string): number | null {
4031
if (globalTracingIntegration === null) {
@@ -52,6 +43,7 @@ function pushActivity(name: string, op: string): number | null {
5243
* popActivity removes a React activity.
5344
* Is a no-op if Tracing integration is not valid.
5445
* @param activity id of activity that is being popped
46+
* @deprecated remove when @sentry/apm no longer used
5547
*/
5648
function popActivity(activity: number | null): void {
5749
if (activity === null || globalTracingIntegration === null) {
@@ -66,6 +58,7 @@ function popActivity(activity: number | null): void {
6658
* Obtain a span given an activity id.
6759
* Is a no-op if Tracing integration is not valid.
6860
* @param activity activity id associated with obtained span
61+
* @deprecated remove when @sentry/apm no longer used
6962
*/
7063
function getActivitySpan(activity: number | null): Span | undefined {
7164
if (activity === null || globalTracingIntegration === null) {
@@ -96,11 +89,9 @@ export type ProfilerProps = {
9689
*/
9790
class Profiler extends React.Component<ProfilerProps> {
9891
// The activity representing how long it takes to mount a component.
99-
public mountActivity: number | null = null;
92+
private _mountActivity: number | null = null;
10093
// The span of the mount activity
101-
public mountSpan: Span | undefined = undefined;
102-
// The span of the render
103-
public renderSpan: Span | undefined = undefined;
94+
private _mountSpan: Span | undefined = undefined;
10495

10596
public static defaultProps: Partial<ProfilerProps> = {
10697
disabled: false,
@@ -116,33 +107,48 @@ class Profiler extends React.Component<ProfilerProps> {
116107
return;
117108
}
118109

110+
// If they are using @sentry/apm, we need to push/pop activities
111+
// tslint:disable-next-line: deprecation
119112
if (getTracingIntegration()) {
120-
this.mountActivity = pushActivity(name, 'mount');
113+
// tslint:disable-next-line: deprecation
114+
this._mountActivity = pushActivity(name, 'mount');
121115
} else {
122-
warnAboutTracing(name);
116+
const activeTransaction = getActiveTransaction();
117+
if (activeTransaction) {
118+
this._mountSpan = activeTransaction.startChild({
119+
description: `<${name}>`,
120+
op: 'react.mount',
121+
});
122+
}
123123
}
124124
}
125125

126126
// If a component mounted, we can finish the mount activity.
127127
public componentDidMount(): void {
128-
this.mountSpan = getActivitySpan(this.mountActivity);
129-
popActivity(this.mountActivity);
130-
this.mountActivity = null;
128+
if (this._mountSpan) {
129+
this._mountSpan.finish();
130+
} else {
131+
// tslint:disable-next-line: deprecation
132+
this._mountSpan = getActivitySpan(this._mountActivity);
133+
// tslint:disable-next-line: deprecation
134+
popActivity(this._mountActivity);
135+
this._mountActivity = null;
136+
}
131137
}
132138

133139
public componentDidUpdate({ updateProps, includeUpdates = true }: ProfilerProps): void {
134140
// Only generate an update span if hasUpdateSpan is true, if there is a valid mountSpan,
135141
// and if the updateProps have changed. It is ok to not do a deep equality check here as it is expensive.
136142
// We are just trying to give baseline clues for further investigation.
137-
if (includeUpdates && this.mountSpan && updateProps !== this.props.updateProps) {
143+
if (includeUpdates && this._mountSpan && updateProps !== this.props.updateProps) {
138144
// See what props haved changed between the previous props, and the current props. This is
139145
// set as data on the span. We just store the prop keys as the values could be potenially very large.
140146
const changedProps = Object.keys(updateProps).filter(k => updateProps[k] !== this.props.updateProps[k]);
141147
if (changedProps.length > 0) {
142148
// The update span is a point in time span with 0 duration, just signifying that the component
143149
// has been updated.
144150
const now = timestampWithMs();
145-
this.mountSpan.startChild({
151+
this._mountSpan.startChild({
146152
data: {
147153
changedProps,
148154
},
@@ -160,14 +166,14 @@ class Profiler extends React.Component<ProfilerProps> {
160166
public componentWillUnmount(): void {
161167
const { name, includeRender = true } = this.props;
162168

163-
if (this.mountSpan && includeRender) {
169+
if (this._mountSpan && includeRender) {
164170
// If we were able to obtain the spanId of the mount activity, we should set the
165171
// next activity as a child to the component mount activity.
166-
this.mountSpan.startChild({
172+
this._mountSpan.startChild({
167173
description: `<${name}>`,
168174
endTimestamp: timestampWithMs(),
169175
op: `react.render`,
170-
startTimestamp: this.mountSpan.endTimestamp,
176+
startTimestamp: this._mountSpan.endTimestamp,
171177
});
172178
}
173179
}
@@ -221,22 +227,26 @@ function useProfiler(
221227
hasRenderSpan: true,
222228
},
223229
): void {
224-
const [mountActivity] = React.useState(() => {
230+
const [mountSpan] = React.useState(() => {
225231
if (options && options.disabled) {
226-
return null;
232+
return undefined;
227233
}
228234

229-
if (getTracingIntegration()) {
230-
return pushActivity(name, 'mount');
235+
const activeTransaction = getActiveTransaction();
236+
if (activeTransaction) {
237+
return activeTransaction.startChild({
238+
description: `<${name}>`,
239+
op: 'react.mount',
240+
});
231241
}
232242

233-
warnAboutTracing(name);
234-
return null;
243+
return undefined;
235244
});
236245

237246
React.useEffect(() => {
238-
const mountSpan = getActivitySpan(mountActivity);
239-
popActivity(mountActivity);
247+
if (mountSpan) {
248+
mountSpan.finish();
249+
}
240250

241251
return () => {
242252
if (mountSpan && options.hasRenderSpan) {
@@ -252,3 +262,15 @@ function useProfiler(
252262
}
253263

254264
export { withProfiler, Profiler, useProfiler };
265+
266+
/** Grabs active transaction off scope */
267+
export function getActiveTransaction<T extends Transaction>(hub: Hub = getCurrentHub()): T | undefined {
268+
if (hub) {
269+
const scope = hub.getScope();
270+
if (scope) {
271+
return scope.getTransaction() as T | undefined;
272+
}
273+
}
274+
275+
return undefined;
276+
}

0 commit comments

Comments
 (0)