Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Add @sentry/tracing #2719

Merged
merged 18 commits into from Jul 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
}