Skip to content

Commit 384ef63

Browse files
committed
fix: Make sure vue and react are backwards compatible with @sentry/apm
1 parent 80edac4 commit 384ef63

File tree

3 files changed

+158
-30
lines changed

3 files changed

+158
-30
lines changed

packages/integrations/src/vue.ts

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
1-
import { EventProcessor, Hub, Integration, Scope, Span, Transaction } 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

4+
/**
5+
* Used to extract Tracing integration from the current client,
6+
* without the need to import `Tracing` itself from the @sentry/apm package.
7+
* @deprecated as @sentry/tracing should be used over @sentry/apm.
8+
*/
9+
const TRACING_GETTER = ({
10+
id: 'Tracing',
11+
} as any) as IntegrationClass<Integration>;
12+
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+
420
/** Global Vue object limited to the methods/attributes we require */
521
interface VueInstance {
622
config: {
@@ -63,7 +79,7 @@ interface TracingOptions {
6379
* Or to an array of specific component names (case-sensitive).
6480
*/
6581
trackComponents: boolean | string[];
66-
/** How long to wait until the tracked root span is marked as finished and sent of to Sentry */
82+
/** How long to wait until the tracked root activity is marked as finished and sent of to Sentry */
6783
timeout: number;
6884
/**
6985
* List of hooks to keep track of during component lifecycle.
@@ -129,6 +145,7 @@ export class Vue implements Integration {
129145
private readonly _componentsCache: { [key: string]: string } = {};
130146
private _rootSpan?: Span;
131147
private _rootSpanTimer?: ReturnType<typeof setTimeout>;
148+
private _tracingActivity?: number;
132149

133150
/**
134151
* @inheritDoc
@@ -212,18 +229,37 @@ export class Vue implements Integration {
212229
// On the first handler call (before), it'll be undefined, as `$once` will add it in the future.
213230
// However, on the second call (after), it'll be already in place.
214231
if (this._rootSpan) {
215-
this._finishRootSpan(now);
232+
this._finishRootSpan(now, getCurrentHub);
216233
} else {
217234
vm.$once(`hook:${hook}`, () => {
218-
// Create an span on the first event call. There'll be no second call, as rootSpan will be in place,
235+
// Create an activity on the first event call. There'll be no second call, as rootSpan will be in place,
219236
// thus new event handler won't be attached.
220237

221-
const activeTransaction = getActiveTransaction(getCurrentHub());
222-
if (activeTransaction) {
223-
this._rootSpan = activeTransaction.startChild({
224-
description: 'Application Render',
225-
op: 'Vue',
226-
});
238+
// We do this whole dance with `TRACING_GETTER` to prevent `@sentry/apm` from becoming a peerDependency.
239+
// We also need to ask for the `.constructor`, as `pushActivity` and `popActivity` are static, not instance methods.
240+
// tslint:disable-next-line: deprecation
241+
const tracingIntegration = getCurrentHub().getIntegration(TRACING_GETTER);
242+
if (tracingIntegration) {
243+
// tslint:disable-next-line:no-unsafe-any
244+
this._tracingActivity = (tracingIntegration as any).constructor.pushActivity('Vue Application Render');
245+
// tslint:disable-next-line:no-unsafe-any
246+
const transaction = (tracingIntegration as any).constructor.getTransaction();
247+
if (transaction) {
248+
// tslint:disable-next-line:no-unsafe-any
249+
this._rootSpan = transaction.startChild({
250+
description: 'Application Render',
251+
op: 'Vue',
252+
});
253+
}
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+
}
227263
}
228264
});
229265
}
@@ -246,7 +282,7 @@ export class Vue implements Integration {
246282
// However, on the second call (after), it'll be already in place.
247283
if (span) {
248284
span.finish();
249-
this._finishRootSpan(now);
285+
this._finishRootSpan(now, getCurrentHub);
250286
} else {
251287
vm.$once(`hook:${hook}`, () => {
252288
if (this._rootSpan) {
@@ -287,13 +323,25 @@ export class Vue implements Integration {
287323
});
288324
};
289325

290-
/** Finish top-level span with a debounce configured using `timeout` option */
291-
private _finishRootSpan(timestamp: number): void {
326+
/** Finish top-level span and activity with a debounce configured using `timeout` option */
327+
private _finishRootSpan(timestamp: number, getCurrentHub: () => Hub): void {
292328
if (this._rootSpanTimer) {
293329
clearTimeout(this._rootSpanTimer);
294330
}
295331

296332
this._rootSpanTimer = setTimeout(() => {
333+
if (this._tracingActivity) {
334+
// We do this whole dance with `TRACING_GETTER` to prevent `@sentry/apm` from becoming a peerDependency.
335+
// We also need to ask for the `.constructor`, as `pushActivity` and `popActivity` are static, not instance methods.
336+
// tslint:disable-next-line: deprecation
337+
const tracingIntegration = getCurrentHub().getIntegration(TRACING_GETTER);
338+
if (tracingIntegration) {
339+
// tslint:disable-next-line:no-unsafe-any
340+
(tracingIntegration as any).constructor.popActivity(this._tracingActivity);
341+
}
342+
}
343+
344+
// We should always finish the span, only should pop activity if using @sentry/apm
297345
if (this._rootSpan) {
298346
this._rootSpan.finish(timestamp);
299347
}
@@ -306,7 +354,8 @@ export class Vue implements Integration {
306354

307355
this._options.Vue.mixin({
308356
beforeCreate(this: ViewModel): void {
309-
if (getActiveTransaction(getCurrentHub())) {
357+
// tslint:disable-next-line: deprecation
358+
if (getCurrentHub().getIntegration(TRACING_GETTER) || getCurrentHub().getIntegration(BROWSER_TRACING_GETTER)) {
310359
// `this` points to currently rendered component
311360
applyTracingHooks(this, getCurrentHub);
312361
} else {

packages/react/src/profiler.tsx

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

77
export const UNKNOWN_COMPONENT = 'unknown';
88

9+
const TRACING_GETTER = ({
10+
id: 'Tracing',
11+
} as any) as IntegrationClass<Integration>;
12+
13+
let globalTracingIntegration: Integration | null = null;
14+
/** @deprecated remove when @sentry/apm no longer used */
15+
const getTracingIntegration = () => {
16+
if (globalTracingIntegration) {
17+
return globalTracingIntegration;
18+
}
19+
20+
globalTracingIntegration = getCurrentHub().getIntegration(TRACING_GETTER);
21+
return globalTracingIntegration;
22+
};
23+
24+
/**
25+
* pushActivity creates an new react activity.
26+
* Is a no-op if Tracing integration is not valid
27+
* @param name displayName of component that started activity
28+
* @deprecated remove when @sentry/apm no longer used
29+
*/
30+
function pushActivity(name: string, op: string): number | null {
31+
if (globalTracingIntegration === null) {
32+
return null;
33+
}
34+
35+
// tslint:disable-next-line:no-unsafe-any
36+
return (globalTracingIntegration as any).constructor.pushActivity(name, {
37+
description: `<${name}>`,
38+
op: `react.${op}`,
39+
});
40+
}
41+
42+
/**
43+
* popActivity removes a React activity.
44+
* Is a no-op if Tracing integration is not valid.
45+
* @param activity id of activity that is being popped
46+
* @deprecated remove when @sentry/apm no longer used
47+
*/
48+
function popActivity(activity: number | null): void {
49+
if (activity === null || globalTracingIntegration === null) {
50+
return;
51+
}
52+
53+
// tslint:disable-next-line:no-unsafe-any
54+
(globalTracingIntegration as any).constructor.popActivity(activity);
55+
}
56+
57+
/**
58+
* Obtain a span given an activity id.
59+
* Is a no-op if Tracing integration is not valid.
60+
* @param activity activity id associated with obtained span
61+
* @deprecated remove when @sentry/apm no longer used
62+
*/
63+
function getActivitySpan(activity: number | null): Span | undefined {
64+
if (activity === null || globalTracingIntegration === null) {
65+
return undefined;
66+
}
67+
68+
// tslint:disable-next-line:no-unsafe-any
69+
return (globalTracingIntegration as any).constructor.getActivitySpan(activity) as Span | undefined;
70+
}
71+
972
export type ProfilerProps = {
1073
// The name of the component being profiled.
1174
name: string;
@@ -25,8 +88,10 @@ export type ProfilerProps = {
2588
* spans based on component lifecycles.
2689
*/
2790
class Profiler extends React.Component<ProfilerProps> {
28-
// The span representing how long it takes to mount a component
29-
public mountSpan: Span | undefined = undefined;
91+
// The activity representing how long it takes to mount a component.
92+
private _mountActivity: number | null = null;
93+
// The span of the mount activity
94+
private _mountSpan: Span | undefined = undefined;
3095

3196
public static defaultProps: Partial<ProfilerProps> = {
3297
disabled: false,
@@ -42,35 +107,48 @@ class Profiler extends React.Component<ProfilerProps> {
42107
return;
43108
}
44109

45-
const activeTransaction = getActiveTransaction();
46-
if (activeTransaction) {
47-
this.mountSpan = activeTransaction.startChild({
48-
description: `<${name}>`,
49-
op: 'react.mount',
50-
});
110+
// If they are using @sentry/apm, we need to push/pop activities
111+
// tslint:disable-next-line: deprecation
112+
if (getTracingIntegration()) {
113+
// tslint:disable-next-line: deprecation
114+
this._mountActivity = pushActivity(name, 'mount');
115+
} else {
116+
const activeTransaction = getActiveTransaction();
117+
if (activeTransaction) {
118+
this._mountSpan = activeTransaction.startChild({
119+
description: `<${name}>`,
120+
op: 'react.mount',
121+
});
122+
}
51123
}
52124
}
53125

54126
// If a component mounted, we can finish the mount activity.
55127
public componentDidMount(): void {
56-
if (this.mountSpan) {
57-
this.mountSpan.finish();
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;
58136
}
59137
}
60138

61139
public componentDidUpdate({ updateProps, includeUpdates = true }: ProfilerProps): void {
62140
// Only generate an update span if hasUpdateSpan is true, if there is a valid mountSpan,
63141
// and if the updateProps have changed. It is ok to not do a deep equality check here as it is expensive.
64142
// We are just trying to give baseline clues for further investigation.
65-
if (includeUpdates && this.mountSpan && updateProps !== this.props.updateProps) {
143+
if (includeUpdates && this._mountSpan && updateProps !== this.props.updateProps) {
66144
// See what props haved changed between the previous props, and the current props. This is
67145
// set as data on the span. We just store the prop keys as the values could be potenially very large.
68146
const changedProps = Object.keys(updateProps).filter(k => updateProps[k] !== this.props.updateProps[k]);
69147
if (changedProps.length > 0) {
70148
// The update span is a point in time span with 0 duration, just signifying that the component
71149
// has been updated.
72150
const now = timestampWithMs();
73-
this.mountSpan.startChild({
151+
this._mountSpan.startChild({
74152
data: {
75153
changedProps,
76154
},
@@ -88,14 +166,14 @@ class Profiler extends React.Component<ProfilerProps> {
88166
public componentWillUnmount(): void {
89167
const { name, includeRender = true } = this.props;
90168

91-
if (this.mountSpan && includeRender) {
169+
if (this._mountSpan && includeRender) {
92170
// If we were able to obtain the spanId of the mount activity, we should set the
93171
// next activity as a child to the component mount activity.
94-
this.mountSpan.startChild({
172+
this._mountSpan.startChild({
95173
description: `<${name}>`,
96174
endTimestamp: timestampWithMs(),
97175
op: `react.render`,
98-
startTimestamp: this.mountSpan.endTimestamp,
176+
startTimestamp: this._mountSpan.endTimestamp,
99177
});
100178
}
101179
}

packages/react/test/profiler.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ let activeTransaction: Record<string, any>;
2626

2727
jest.mock('@sentry/browser', () => ({
2828
getCurrentHub: () => ({
29+
getIntegration: () => undefined,
2930
getScope: () => ({
3031
getTransaction: () => activeTransaction,
3132
}),

0 commit comments

Comments
 (0)