Skip to content

Commit

Permalink
Feat: Add @sentry/tracing (#2719)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
AbhiPrasad committed Jul 17, 2020
1 parent 0f07871 commit 820bd77
Show file tree
Hide file tree
Showing 43 changed files with 3,919 additions and 121 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Expand Up @@ -3,6 +3,8 @@
## Unreleased

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

## 5.19.2

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

## 5.19.0

Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -28,6 +28,7 @@
"packages/minimal",
"packages/node",
"packages/react",
"packages/tracing",
"packages/types",
"packages/typescript",
"packages/utils"
Expand Down
50 changes: 45 additions & 5 deletions packages/integrations/src/vue.ts
@@ -1,14 +1,22 @@
import { EventProcessor, Hub, Integration, IntegrationClass, Span } from '@sentry/types';
import { EventProcessor, Hub, Integration, IntegrationClass, Scope, Span, Transaction } from '@sentry/types';
import { basename, getGlobalObject, logger, timestampWithMs } from '@sentry/utils';

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

/**
* Used to extract BrowserTracing integration from @sentry/tracing
*/
const BROWSER_TRACING_GETTER = ({
id: 'BrowserTracing',
} as any) as IntegrationClass<Integration>;

/** Global Vue object limited to the methods/attributes we require */
interface VueInstance {
config: {
Expand Down Expand Up @@ -229,6 +237,7 @@ export class Vue implements Integration {

// We do this whole dance with `TRACING_GETTER` to prevent `@sentry/apm` from becoming a peerDependency.
// We also need to ask for the `.constructor`, as `pushActivity` and `popActivity` are static, not instance methods.
// tslint:disable-next-line: deprecation
const tracingIntegration = getCurrentHub().getIntegration(TRACING_GETTER);
if (tracingIntegration) {
// tslint:disable-next-line:no-unsafe-any
Expand All @@ -242,6 +251,15 @@ export class Vue implements Integration {
op: 'Vue',
});
}
// Use functionality from @sentry/tracing
} else {
const activeTransaction = getActiveTransaction(getCurrentHub());
if (activeTransaction) {
this._rootSpan = activeTransaction.startChild({
description: 'Application Render',
op: 'Vue',
});
}
}
});
}
Expand Down Expand Up @@ -315,15 +333,18 @@ export class Vue implements Integration {
if (this._tracingActivity) {
// We do this whole dance with `TRACING_GETTER` to prevent `@sentry/apm` from becoming a peerDependency.
// We also need to ask for the `.constructor`, as `pushActivity` and `popActivity` are static, not instance methods.
// tslint:disable-next-line: deprecation
const tracingIntegration = getCurrentHub().getIntegration(TRACING_GETTER);
if (tracingIntegration) {
// tslint:disable-next-line:no-unsafe-any
(tracingIntegration as any).constructor.popActivity(this._tracingActivity);
if (this._rootSpan) {
this._rootSpan.finish(timestamp);
}
}
}

// We should always finish the span, only should pop activity if using @sentry/apm
if (this._rootSpan) {
this._rootSpan.finish(timestamp);
}
}, this._options.tracingOptions.timeout);
}

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

this._options.Vue.mixin({
beforeCreate(this: ViewModel): void {
if (getCurrentHub().getIntegration(TRACING_GETTER)) {
// tslint:disable-next-line: deprecation
if (getCurrentHub().getIntegration(TRACING_GETTER) || getCurrentHub().getIntegration(BROWSER_TRACING_GETTER)) {
// `this` points to currently rendered component
applyTracingHooks(this, getCurrentHub);
} else {
Expand Down Expand Up @@ -405,3 +427,21 @@ export class Vue implements Integration {
}
}
}

// tslint:disable-next-line: completed-docs
interface HubType extends Hub {
// tslint:disable-next-line: completed-docs
getScope?(): Scope | undefined;
}

/** Grabs active transaction off scope */
export function getActiveTransaction<T extends Transaction>(hub: HubType): T | undefined {
if (hub && hub.getScope) {
const scope = hub.getScope() as Scope;
if (scope) {
return scope.getTransaction() as T | undefined;
}
}

return undefined;
}
94 changes: 58 additions & 36 deletions packages/react/src/profiler.tsx
@@ -1,6 +1,6 @@
import { getCurrentHub } from '@sentry/browser';
import { Integration, IntegrationClass, Span } from '@sentry/types';
import { logger, timestampWithMs } from '@sentry/utils';
import { getCurrentHub, Hub } from '@sentry/browser';
import { Integration, IntegrationClass, Span, Transaction } from '@sentry/types';
import { timestampWithMs } from '@sentry/utils';
import * as hoistNonReactStatic from 'hoist-non-react-statics';
import * as React from 'react';

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

let globalTracingIntegration: Integration | null = null;
/** @deprecated remove when @sentry/apm no longer used */
const getTracingIntegration = () => {
if (globalTracingIntegration) {
return globalTracingIntegration;
Expand All @@ -20,21 +21,11 @@ const getTracingIntegration = () => {
return globalTracingIntegration;
};

/**
* Warn if tracing integration not configured. Will only warn once.
*/
function warnAboutTracing(name: string): void {
if (globalTracingIntegration === null) {
logger.warn(
`Unable to profile component ${name} due to invalid Tracing Integration. Please make sure the Tracing integration is setup properly.`,
);
}
}

/**
* pushActivity creates an new react activity.
* Is a no-op if Tracing integration is not valid
* @param name displayName of component that started activity
* @deprecated remove when @sentry/apm no longer used
*/
function pushActivity(name: string, op: string): number | null {
if (globalTracingIntegration === null) {
Expand All @@ -52,6 +43,7 @@ function pushActivity(name: string, op: string): number | null {
* popActivity removes a React activity.
* Is a no-op if Tracing integration is not valid.
* @param activity id of activity that is being popped
* @deprecated remove when @sentry/apm no longer used
*/
function popActivity(activity: number | null): void {
if (activity === null || globalTracingIntegration === null) {
Expand All @@ -66,6 +58,7 @@ function popActivity(activity: number | null): void {
* Obtain a span given an activity id.
* Is a no-op if Tracing integration is not valid.
* @param activity activity id associated with obtained span
* @deprecated remove when @sentry/apm no longer used
*/
function getActivitySpan(activity: number | null): Span | undefined {
if (activity === null || globalTracingIntegration === null) {
Expand Down Expand Up @@ -96,11 +89,9 @@ export type ProfilerProps = {
*/
class Profiler extends React.Component<ProfilerProps> {
// The activity representing how long it takes to mount a component.
public mountActivity: number | null = null;
private _mountActivity: number | null = null;
// The span of the mount activity
public mountSpan: Span | undefined = undefined;
// The span of the render
public renderSpan: Span | undefined = undefined;
private _mountSpan: Span | undefined = undefined;

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

// If they are using @sentry/apm, we need to push/pop activities
// tslint:disable-next-line: deprecation
if (getTracingIntegration()) {
this.mountActivity = pushActivity(name, 'mount');
// tslint:disable-next-line: deprecation
this._mountActivity = pushActivity(name, 'mount');
} else {
warnAboutTracing(name);
const activeTransaction = getActiveTransaction();
if (activeTransaction) {
this._mountSpan = activeTransaction.startChild({
description: `<${name}>`,
op: 'react.mount',
});
}
}
}

// If a component mounted, we can finish the mount activity.
public componentDidMount(): void {
this.mountSpan = getActivitySpan(this.mountActivity);
popActivity(this.mountActivity);
this.mountActivity = null;
if (this._mountSpan) {
this._mountSpan.finish();
} else {
// tslint:disable-next-line: deprecation
this._mountSpan = getActivitySpan(this._mountActivity);
// tslint:disable-next-line: deprecation
popActivity(this._mountActivity);
this._mountActivity = null;
}
}

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

if (this.mountSpan && includeRender) {
if (this._mountSpan && includeRender) {
// If we were able to obtain the spanId of the mount activity, we should set the
// next activity as a child to the component mount activity.
this.mountSpan.startChild({
this._mountSpan.startChild({
description: `<${name}>`,
endTimestamp: timestampWithMs(),
op: `react.render`,
startTimestamp: this.mountSpan.endTimestamp,
startTimestamp: this._mountSpan.endTimestamp,
});
}
}
Expand Down Expand Up @@ -221,22 +227,26 @@ function useProfiler(
hasRenderSpan: true,
},
): void {
const [mountActivity] = React.useState(() => {
const [mountSpan] = React.useState(() => {
if (options && options.disabled) {
return null;
return undefined;
}

if (getTracingIntegration()) {
return pushActivity(name, 'mount');
const activeTransaction = getActiveTransaction();
if (activeTransaction) {
return activeTransaction.startChild({
description: `<${name}>`,
op: 'react.mount',
});
}

warnAboutTracing(name);
return null;
return undefined;
});

React.useEffect(() => {
const mountSpan = getActivitySpan(mountActivity);
popActivity(mountActivity);
if (mountSpan) {
mountSpan.finish();
}

return () => {
if (mountSpan && options.hasRenderSpan) {
Expand All @@ -252,3 +262,15 @@ function useProfiler(
}

export { withProfiler, Profiler, useProfiler };

/** Grabs active transaction off scope */
export function getActiveTransaction<T extends Transaction>(hub: Hub = getCurrentHub()): T | undefined {
if (hub) {
const scope = hub.getScope();
if (scope) {
return scope.getTransaction() as T | undefined;
}
}

return undefined;
}

0 comments on commit 820bd77

Please sign in to comment.