Skip to content

Commit 75913f0

Browse files
committed
ref: Rework rest of integration
1 parent 4eddf1a commit 75913f0

File tree

2 files changed

+112
-38
lines changed

2 files changed

+112
-38
lines changed

packages/apm/src/integrations/tracing.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -769,12 +769,14 @@ export class Tracing implements Integration {
769769
* @param name Name of the activity, can be any string (Only used internally to identify the activity)
770770
* @param spanContext If provided a Span with the SpanContext will be created.
771771
* @param options _autoPopAfter_ | Time in ms, if provided the activity will be popped automatically after this timeout. This can be helpful in cases where you cannot gurantee your application knows the state and calls `popActivity` for sure.
772+
* @param options _parentSpanId_ | Set a custom parent span id for the activity's span.
772773
*/
773774
public static pushActivity(
774775
name: string,
775776
spanContext?: SpanContext,
776777
options?: {
777778
autoPopAfter?: number;
779+
parentSpanId?: string;
778780
},
779781
): number {
780782
const activeTransaction = Tracing._activeTransaction;
@@ -789,6 +791,9 @@ export class Tracing implements Integration {
789791
const hub = _getCurrentHub();
790792
if (hub) {
791793
const span = activeTransaction.startChild(spanContext);
794+
if (options && options.parentSpanId) {
795+
span.parentSpanId = options.parentSpanId;
796+
}
792797
Tracing._activities[Tracing._currentIndex] = {
793798
name,
794799
span,
@@ -817,8 +822,12 @@ export class Tracing implements Integration {
817822

818823
/**
819824
* Removes activity and finishes the span in case there is one
825+
* @param id the id of the activity being removed
826+
* @param spanData span data that can be updated
827+
* @param finish if a span should be finished after the activity is removed
828+
*
820829
*/
821-
public static popActivity(id: number, spanData?: { [key: string]: any }): void {
830+
public static popActivity(id: number, spanData?: { [key: string]: any }, finish: boolean = true): void {
822831
// The !id is on purpose to also fail with 0
823832
// Since 0 is returned by push activity in case there is no active transaction
824833
if (!id) {
@@ -845,7 +854,9 @@ export class Tracing implements Integration {
845854
if (Tracing.options && Tracing.options.debug && Tracing.options.debug.spanDebugTimingInfo) {
846855
Tracing._addSpanDebugInfo(span);
847856
}
848-
span.finish();
857+
if (finish) {
858+
span.finish();
859+
}
849860
}
850861
// tslint:disable-next-line: no-dynamic-delete
851862
delete Tracing._activities[id];
@@ -866,6 +877,22 @@ export class Tracing implements Integration {
866877
}, timeout);
867878
}
868879
}
880+
881+
/**
882+
* Get span based on activity id
883+
*/
884+
public static getActivitySpan(id: number): Span | undefined {
885+
if (!id) {
886+
return undefined;
887+
}
888+
if (Tracing._getCurrentHub) {
889+
const hub = Tracing._getCurrentHub();
890+
if (hub) {
891+
return Tracing._activities[id].span;
892+
}
893+
}
894+
return undefined;
895+
}
869896
}
870897

871898
/**

packages/react/src/profiler.tsx

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

@@ -49,20 +49,31 @@ const getTracingIntegration = () => {
4949
return globalTracingIntegration;
5050
};
5151

52-
/** JSDOC */
52+
/**
53+
* Warn if tracing integration not configured. Will only warn once.
54+
*/
5355
function warnAboutTracing(name: string): void {
5456
if (globalTracingIntegration === null) {
5557
logger.warn(
56-
`Unable to profile component ${name} due to invalid Tracing Integration. Please make sure to setup the Tracing integration.`,
58+
`Unable to profile component ${name} due to invalid Tracing Integration. Please make sure the Tracing integration is setup properly.`,
5759
);
5860
}
5961
}
6062

6163
/**
62-
* pushActivity creates an new react activity
64+
* pushActivity creates an new react activity.
65+
* Is a no-op if Tracing integration is not valid
6366
* @param name displayName of component that started activity
6467
*/
65-
const pushActivity = (name: string, op: string, options?: Object): number | null => {
68+
function pushActivity(
69+
name: string,
70+
op: string,
71+
context?: SpanContext,
72+
options?: {
73+
autoPopAfter?: number;
74+
parentSpanId?: string;
75+
},
76+
): number | null {
6677
if (globalTracingIntegration === null) {
6778
return null;
6879
}
@@ -73,47 +84,61 @@ const pushActivity = (name: string, op: string, options?: Object): number | null
7384
{
7485
description: `<${name}>`,
7586
op: `react.${op}`,
87+
...context,
7688
},
7789
options,
7890
);
79-
};
91+
}
8092

8193
/**
82-
* popActivity removes a React activity if it exists
94+
* popActivity removes a React activity.
95+
* Is a no-op if invalid Tracing integration or invalid activity id.
8396
* @param activity id of activity that is being popped
97+
* @param finish if a span should be finished after the activity is removed
8498
*/
85-
const popActivity = (activity: number | null): void => {
99+
function popActivity(activity: number | null, finish: boolean = true): void {
86100
if (activity === null || globalTracingIntegration === null) {
87101
return;
88102
}
89103

90104
// tslint:disable-next-line:no-unsafe-any
91-
(globalTracingIntegration as any).constructor.popActivity(activity);
92-
};
105+
(globalTracingIntegration as any).constructor.popActivity(activity, undefined, finish);
106+
}
107+
108+
function getActivitySpan(activity: number | null): Span | undefined {
109+
if (globalTracingIntegration === null) {
110+
return undefined;
111+
}
112+
113+
// tslint:disable-next-line:no-unsafe-any
114+
return (globalTracingIntegration as any).constructor.getActivitySpan(activity) as Span | undefined;
115+
}
93116

94117
export type ProfilerProps = {
95118
// The name of the component being profiled.
96119
name: string;
97120
// If the Profiler is disabled. False by default.
98121
disabled?: boolean;
122+
// If component updates should be displayed as spans. False by default.
123+
generateUpdateSpans?: boolean;
99124
};
100125

101126
/**
102127
* The Profiler component leverages Sentry's Tracing integration to generate
103128
* spans based on component lifecycles.
104129
*/
105130
class Profiler extends React.Component<ProfilerProps> {
106-
public mountInfo: {
107-
// The activity representing when a component was mounted onto a page.
108-
activity: number | null;
109-
// The span from the mountInfo activity
110-
span: Span | null;
111-
} = {
112-
activity: null,
113-
span: null,
114-
};
131+
// The activity representing how long it takes to mount a component.
132+
public mountActivity: number | null = null;
133+
// The spanId of the mount activity
134+
public mountSpanId: string | null = null;
115135
// The activity representing how long a component was on the page.
116-
public visibleActivity: number | null = null;
136+
public renderActivity: number | null = null;
137+
138+
public static defaultProps: Partial<ProfilerProps> = {
139+
disabled: false,
140+
generateUpdateSpans: false,
141+
};
117142

118143
public constructor(props: ProfilerProps) {
119144
super(props);
@@ -124,7 +149,7 @@ class Profiler extends React.Component<ProfilerProps> {
124149
}
125150

126151
if (getTracingIntegration()) {
127-
this.mountInfo.activity = pushActivity(name, 'mount');
152+
this.mountActivity = pushActivity(name, 'mount');
128153
} else {
129154
warnAboutTracing(name);
130155
}
@@ -133,23 +158,44 @@ class Profiler extends React.Component<ProfilerProps> {
133158
// If a component mounted, we can finish the mount activity.
134159
public componentDidMount(): void {
135160
afterNextFrame(() => {
136-
popActivity(this.mountInfo.activity);
137-
this.mountInfo.activity = null;
161+
const span = getActivitySpan(this.mountActivity);
162+
if (span) {
163+
this.mountSpanId = span.spanId;
164+
}
165+
popActivity(this.mountActivity);
166+
this.mountActivity = null;
138167

139-
this.visibleActivity = pushActivity(this.props.name, 'visible');
168+
// If we were able to obtain the spanId of the mount activity, we should set the
169+
// next activity as a child to the component mount activity.
170+
const options = span ? { parentSpanId: span.spanId } : {};
171+
this.renderActivity = pushActivity(this.props.name, 'render', {}, options);
140172
});
141173
}
142174

143-
// If a component doesn't mount, the visible activity will be end when the
175+
public componentDidUpdate(prevProps: ProfilerProps): void {
176+
if (prevProps.generateUpdateSpans && this.mountSpanId) {
177+
const now = timestampWithMs();
178+
const updateActivity = pushActivity(
179+
prevProps.name,
180+
'update',
181+
{
182+
endTimestamp: now,
183+
startTimestamp: now,
184+
},
185+
{ parentSpanId: this.mountSpanId },
186+
);
187+
popActivity(updateActivity, false);
188+
}
189+
}
190+
191+
// If a component doesn't mount, the render activity will be end when the
144192
public componentWillUnmount(): void {
145193
afterNextFrame(() => {
146-
popActivity(this.visibleActivity);
147-
this.visibleActivity = null;
194+
popActivity(this.renderActivity, false);
195+
this.renderActivity = null;
148196
});
149197
}
150198

151-
public finishProfile = () => {};
152-
153199
public render(): React.ReactNode {
154200
return this.props.children;
155201
}
@@ -191,15 +237,16 @@ function withProfiler<P extends object>(
191237
* @param name displayName of component being profiled
192238
*/
193239
function useProfiler(name: string): void {
194-
const [activity] = React.useState(() => pushActivity(name));
240+
const [activity] = React.useState(() => pushActivity(name, 'mount'));
195241

196242
React.useEffect(() => {
197243
afterNextFrame(() => {
198-
const tracingIntegration = getCurrentHub().getIntegration(TRACING_GETTER);
199-
if (tracingIntegration !== null) {
200-
// tslint:disable-next-line:no-unsafe-any
201-
(tracingIntegration as any).constructor.popActivity(activity);
202-
}
244+
popActivity(activity);
245+
const renderActivity = pushActivity(name, 'render');
246+
247+
return () => {
248+
popActivity(renderActivity);
249+
};
203250
});
204251
}, []);
205252
}

0 commit comments

Comments
 (0)