Skip to content

Commit eb3917e

Browse files
authored
ref: Convert React and Vue Tracing to use active transaction (#2741)
* ref: Make React Profiler use active transaction * ref: Make Vue Tracing using active transaction
1 parent 28a0f91 commit eb3917e

File tree

3 files changed

+121
-213
lines changed

3 files changed

+121
-213
lines changed

packages/integrations/src/vue.ts

Lines changed: 34 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
1-
import { EventProcessor, Hub, Integration, IntegrationClass, Span } from '@sentry/types';
1+
import { EventProcessor, Hub, Integration, 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-
*/
8-
const TRACING_GETTER = ({
9-
id: 'Tracing',
10-
} as any) as IntegrationClass<Integration>;
11-
124
/** Global Vue object limited to the methods/attributes we require */
135
interface VueInstance {
146
config: {
@@ -71,7 +63,7 @@ interface TracingOptions {
7163
* Or to an array of specific component names (case-sensitive).
7264
*/
7365
trackComponents: boolean | string[];
74-
/** How long to wait until the tracked root activity is marked as finished and sent of to Sentry */
66+
/** How long to wait until the tracked root span is marked as finished and sent of to Sentry */
7567
timeout: number;
7668
/**
7769
* List of hooks to keep track of during component lifecycle.
@@ -137,7 +129,6 @@ export class Vue implements Integration {
137129
private readonly _componentsCache: { [key: string]: string } = {};
138130
private _rootSpan?: Span;
139131
private _rootSpanTimer?: ReturnType<typeof setTimeout>;
140-
private _tracingActivity?: number;
141132

142133
/**
143134
* @inheritDoc
@@ -221,27 +212,18 @@ export class Vue implements Integration {
221212
// On the first handler call (before), it'll be undefined, as `$once` will add it in the future.
222213
// However, on the second call (after), it'll be already in place.
223214
if (this._rootSpan) {
224-
this._finishRootSpan(now, getCurrentHub);
215+
this._finishRootSpan(now);
225216
} else {
226217
vm.$once(`hook:${hook}`, () => {
227-
// Create an activity on the first event call. There'll be no second call, as rootSpan will be in place,
218+
// Create an span on the first event call. There'll be no second call, as rootSpan will be in place,
228219
// thus new event handler won't be attached.
229220

230-
// We do this whole dance with `TRACING_GETTER` to prevent `@sentry/apm` from becoming a peerDependency.
231-
// We also need to ask for the `.constructor`, as `pushActivity` and `popActivity` are static, not instance methods.
232-
const tracingIntegration = getCurrentHub().getIntegration(TRACING_GETTER);
233-
if (tracingIntegration) {
234-
// tslint:disable-next-line:no-unsafe-any
235-
this._tracingActivity = (tracingIntegration as any).constructor.pushActivity('Vue Application Render');
236-
// tslint:disable-next-line:no-unsafe-any
237-
const transaction = (tracingIntegration as any).constructor.getTransaction();
238-
if (transaction) {
239-
// tslint:disable-next-line:no-unsafe-any
240-
this._rootSpan = transaction.startChild({
241-
description: 'Application Render',
242-
op: 'Vue',
243-
});
244-
}
221+
const activeTransaction = getActiveTransaction(getCurrentHub());
222+
if (activeTransaction) {
223+
this._rootSpan = activeTransaction.startChild({
224+
description: 'Application Render',
225+
op: 'Vue',
226+
});
245227
}
246228
});
247229
}
@@ -264,7 +246,7 @@ export class Vue implements Integration {
264246
// However, on the second call (after), it'll be already in place.
265247
if (span) {
266248
span.finish();
267-
this._finishRootSpan(now, getCurrentHub);
249+
this._finishRootSpan(now);
268250
} else {
269251
vm.$once(`hook:${hook}`, () => {
270252
if (this._rootSpan) {
@@ -305,24 +287,15 @@ export class Vue implements Integration {
305287
});
306288
};
307289

308-
/** Finish top-level span and activity with a debounce configured using `timeout` option */
309-
private _finishRootSpan(timestamp: number, getCurrentHub: () => Hub): void {
290+
/** Finish top-level span with a debounce configured using `timeout` option */
291+
private _finishRootSpan(timestamp: number): void {
310292
if (this._rootSpanTimer) {
311293
clearTimeout(this._rootSpanTimer);
312294
}
313295

314296
this._rootSpanTimer = setTimeout(() => {
315-
if (this._tracingActivity) {
316-
// We do this whole dance with `TRACING_GETTER` to prevent `@sentry/apm` from becoming a peerDependency.
317-
// We also need to ask for the `.constructor`, as `pushActivity` and `popActivity` are static, not instance methods.
318-
const tracingIntegration = getCurrentHub().getIntegration(TRACING_GETTER);
319-
if (tracingIntegration) {
320-
// tslint:disable-next-line:no-unsafe-any
321-
(tracingIntegration as any).constructor.popActivity(this._tracingActivity);
322-
if (this._rootSpan) {
323-
this._rootSpan.finish(timestamp);
324-
}
325-
}
297+
if (this._rootSpan) {
298+
this._rootSpan.finish(timestamp);
326299
}
327300
}, this._options.tracingOptions.timeout);
328301
}
@@ -333,7 +306,7 @@ export class Vue implements Integration {
333306

334307
this._options.Vue.mixin({
335308
beforeCreate(this: ViewModel): void {
336-
if (getCurrentHub().getIntegration(TRACING_GETTER)) {
309+
if (getActiveTransaction(getCurrentHub())) {
337310
// `this` points to currently rendered component
338311
applyTracingHooks(this, getCurrentHub);
339312
} else {
@@ -405,3 +378,21 @@ export class Vue implements Integration {
405378
}
406379
}
407380
}
381+
382+
// tslint:disable-next-line: completed-docs
383+
interface HubType extends Hub {
384+
// tslint:disable-next-line: completed-docs
385+
getScope?(): Scope | undefined;
386+
}
387+
388+
/** Grabs active transaction off scope */
389+
export function getActiveTransaction<T extends Transaction>(hub: HubType): T | undefined {
390+
if (hub && hub.getScope) {
391+
const scope = hub.getScope() as Scope;
392+
if (scope) {
393+
return scope.getTransaction() as T | undefined;
394+
}
395+
}
396+
397+
return undefined;
398+
}

packages/react/src/profiler.tsx

Lines changed: 37 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,11 @@
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 { 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

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-
const getTracingIntegration = () => {
15-
if (globalTracingIntegration) {
16-
return globalTracingIntegration;
17-
}
18-
19-
globalTracingIntegration = getCurrentHub().getIntegration(TRACING_GETTER);
20-
return globalTracingIntegration;
21-
};
22-
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-
34-
/**
35-
* pushActivity creates an new react activity.
36-
* Is a no-op if Tracing integration is not valid
37-
* @param name displayName of component that started activity
38-
*/
39-
function pushActivity(name: string, op: string): number | null {
40-
if (globalTracingIntegration === null) {
41-
return null;
42-
}
43-
44-
// tslint:disable-next-line:no-unsafe-any
45-
return (globalTracingIntegration as any).constructor.pushActivity(name, {
46-
description: `<${name}>`,
47-
op: `react.${op}`,
48-
});
49-
}
50-
51-
/**
52-
* popActivity removes a React activity.
53-
* Is a no-op if Tracing integration is not valid.
54-
* @param activity id of activity that is being popped
55-
*/
56-
function popActivity(activity: number | null): void {
57-
if (activity === null || globalTracingIntegration === null) {
58-
return;
59-
}
60-
61-
// tslint:disable-next-line:no-unsafe-any
62-
(globalTracingIntegration as any).constructor.popActivity(activity);
63-
}
64-
65-
/**
66-
* Obtain a span given an activity id.
67-
* Is a no-op if Tracing integration is not valid.
68-
* @param activity activity id associated with obtained span
69-
*/
70-
function getActivitySpan(activity: number | null): Span | undefined {
71-
if (activity === null || globalTracingIntegration === null) {
72-
return undefined;
73-
}
74-
75-
// tslint:disable-next-line:no-unsafe-any
76-
return (globalTracingIntegration as any).constructor.getActivitySpan(activity) as Span | undefined;
77-
}
78-
799
export type ProfilerProps = {
8010
// The name of the component being profiled.
8111
name: string;
@@ -95,12 +25,8 @@ export type ProfilerProps = {
9525
* spans based on component lifecycles.
9626
*/
9727
class Profiler extends React.Component<ProfilerProps> {
98-
// The activity representing how long it takes to mount a component.
99-
public mountActivity: number | null = null;
100-
// The span of the mount activity
28+
// The span representing how long it takes to mount a component
10129
public mountSpan: Span | undefined = undefined;
102-
// The span of the render
103-
public renderSpan: Span | undefined = undefined;
10430

10531
public static defaultProps: Partial<ProfilerProps> = {
10632
disabled: false,
@@ -116,18 +42,20 @@ class Profiler extends React.Component<ProfilerProps> {
11642
return;
11743
}
11844

119-
if (getTracingIntegration()) {
120-
this.mountActivity = pushActivity(name, 'mount');
121-
} else {
122-
warnAboutTracing(name);
45+
const activeTransaction = getActiveTransaction();
46+
if (activeTransaction) {
47+
this.mountSpan = activeTransaction.startChild({
48+
description: `<${name}>`,
49+
op: 'react.mount',
50+
});
12351
}
12452
}
12553

12654
// If a component mounted, we can finish the mount activity.
12755
public componentDidMount(): void {
128-
this.mountSpan = getActivitySpan(this.mountActivity);
129-
popActivity(this.mountActivity);
130-
this.mountActivity = null;
56+
if (this.mountSpan) {
57+
this.mountSpan.finish();
58+
}
13159
}
13260

13361
public componentDidUpdate({ updateProps, includeUpdates = true }: ProfilerProps): void {
@@ -221,22 +149,26 @@ function useProfiler(
221149
hasRenderSpan: true,
222150
},
223151
): void {
224-
const [mountActivity] = React.useState(() => {
152+
const [mountSpan] = React.useState(() => {
225153
if (options && options.disabled) {
226-
return null;
154+
return undefined;
227155
}
228156

229-
if (getTracingIntegration()) {
230-
return pushActivity(name, 'mount');
157+
const activeTransaction = getActiveTransaction();
158+
if (activeTransaction) {
159+
return activeTransaction.startChild({
160+
description: `<${name}>`,
161+
op: 'react.mount',
162+
});
231163
}
232164

233-
warnAboutTracing(name);
234-
return null;
165+
return undefined;
235166
});
236167

237168
React.useEffect(() => {
238-
const mountSpan = getActivitySpan(mountActivity);
239-
popActivity(mountActivity);
169+
if (mountSpan) {
170+
mountSpan.finish();
171+
}
240172

241173
return () => {
242174
if (mountSpan && options.hasRenderSpan) {
@@ -252,3 +184,15 @@ function useProfiler(
252184
}
253185

254186
export { withProfiler, Profiler, useProfiler };
187+
188+
/** Grabs active transaction off scope */
189+
export function getActiveTransaction<T extends Transaction>(hub: Hub = getCurrentHub()): T | undefined {
190+
if (hub) {
191+
const scope = hub.getScope();
192+
if (scope) {
193+
return scope.getTransaction() as T | undefined;
194+
}
195+
}
196+
197+
return undefined;
198+
}

0 commit comments

Comments
 (0)