Skip to content

[VERY WIP] ref(tracing): Restructure Tracing Integration #2705

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

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/hub/src/hub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,8 +376,8 @@ export class Hub implements HubInterface {
/**
* @inheritDoc
*/
public startTransaction(context: TransactionContext): Transaction {
return this._callExtensionMethod('startTransaction', context);
public startTransaction(context: TransactionContext, idleTimeout?: number): Transaction {
return this._callExtensionMethod('startTransaction', context, idleTimeout);
}

/**
Expand Down
63 changes: 26 additions & 37 deletions packages/integrations/src/vue.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { EventProcessor, Hub, Integration, IntegrationClass, Span } from '@sentry/types';
import { EventProcessor, Hub, Integration, 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.
*/
const TRACING_GETTER = ({
id: 'Tracing',
} as any) as IntegrationClass<Integration>;
// tslint:disable-next-line: completed-docs
function getActiveTransaction(hub: Hub & { getScope?(): Scope }): Transaction | undefined {
if (!hub.getScope) {
return undefined;
}

const scope = hub.getScope();
if (scope) {
return scope.getTransaction();
}

return undefined;
}

/** Global Vue object limited to the methods/attributes we require */
interface VueInstance {
Expand Down Expand Up @@ -137,7 +143,6 @@ export class Vue implements Integration {
private readonly _componentsCache: { [key: string]: string } = {};
private _rootSpan?: Span;
private _rootSpanTimer?: ReturnType<typeof setTimeout>;
private _tracingActivity?: number;

/**
* @inheritDoc
Expand Down Expand Up @@ -221,27 +226,20 @@ export class Vue implements Integration {
// On the first handler call (before), it'll be undefined, as `$once` will add it in the future.
// However, on the second call (after), it'll be already in place.
if (this._rootSpan) {
this._finishRootSpan(now, getCurrentHub);
this._finishRootSpan(now);
} else {
vm.$once(`hook:${hook}`, () => {
// Create an activity on the first event call. There'll be no second call, as rootSpan will be in place,
// thus new event handler won't be attached.

// 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.
const tracingIntegration = getCurrentHub().getIntegration(TRACING_GETTER);
if (tracingIntegration) {
// tslint:disable-next-line:no-unsafe-any
this._tracingActivity = (tracingIntegration as any).constructor.pushActivity('Vue Application Render');
// tslint:disable-next-line:no-unsafe-any
const transaction = (tracingIntegration as any).constructor.getTransaction();
if (transaction) {
// tslint:disable-next-line:no-unsafe-any
this._rootSpan = transaction.startChild({
description: 'Application Render',
op: 'Vue',
});
}
const activeTransaction = getActiveTransaction(getCurrentHub());
if (activeTransaction) {
this._rootSpan = activeTransaction.startChild({
description: 'Application Render',
op: 'Vue',
});
}
});
}
Expand All @@ -264,7 +262,7 @@ export class Vue implements Integration {
// However, on the second call (after), it'll be already in place.
if (span) {
span.finish();
this._finishRootSpan(now, getCurrentHub);
this._finishRootSpan(now);
} else {
vm.$once(`hook:${hook}`, () => {
if (this._rootSpan) {
Expand Down Expand Up @@ -306,23 +304,14 @@ export class Vue implements Integration {
};

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

this._rootSpanTimer = setTimeout(() => {
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.
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);
}
}
if (this._rootSpan) {
this._rootSpan.finish(timestamp);
}
}, this._options.tracingOptions.timeout);
}
Expand All @@ -333,7 +322,7 @@ export class Vue implements Integration {

this._options.Vue.mixin({
beforeCreate(this: ViewModel): void {
if (getCurrentHub().getIntegration(TRACING_GETTER)) {
if (getActiveTransaction(getCurrentHub())) {
// `this` points to currently rendered component
applyTracingHooks(this, getCurrentHub);
} else {
Expand Down
116 changes: 31 additions & 85 deletions packages/react/src/profiler.tsx
Original file line number Diff line number Diff line change
@@ -1,79 +1,19 @@
import { getCurrentHub } from '@sentry/browser';
import { Integration, IntegrationClass, Span } from '@sentry/types';
import { logger, timestampWithMs } from '@sentry/utils';
import { Span, Transaction } from '@sentry/types';
import { timestampWithMs } from '@sentry/utils';
import * as hoistNonReactStatic from 'hoist-non-react-statics';
import * as React from 'react';

export const UNKNOWN_COMPONENT = 'unknown';

const TRACING_GETTER = ({
id: 'Tracing',
} as any) as IntegrationClass<Integration>;

let globalTracingIntegration: Integration | null = null;
const getTracingIntegration = () => {
if (globalTracingIntegration) {
return globalTracingIntegration;
}

globalTracingIntegration = getCurrentHub().getIntegration(TRACING_GETTER);
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
*/
function pushActivity(name: string, op: string): number | null {
if (globalTracingIntegration === null) {
return null;
function getActiveTransaction(): Transaction | undefined {
const hub = getCurrentHub();
const scope = hub.getScope();
if (scope) {
return scope.getTransaction();
}

// tslint:disable-next-line:no-unsafe-any
return (globalTracingIntegration as any).constructor.pushActivity(name, {
description: `<${name}>`,
op: `react.${op}`,
});
}

/**
* popActivity removes a React activity.
* Is a no-op if Tracing integration is not valid.
* @param activity id of activity that is being popped
*/
function popActivity(activity: number | null): void {
if (activity === null || globalTracingIntegration === null) {
return;
}

// tslint:disable-next-line:no-unsafe-any
(globalTracingIntegration as any).constructor.popActivity(activity);
}

/**
* 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
*/
function getActivitySpan(activity: number | null): Span | undefined {
if (activity === null || globalTracingIntegration === null) {
return undefined;
}

// tslint:disable-next-line:no-unsafe-any
return (globalTracingIntegration as any).constructor.getActivitySpan(activity) as Span | undefined;
return undefined;
}

export type ProfilerProps = {
Expand All @@ -95,8 +35,6 @@ export type ProfilerProps = {
* spans based on component lifecycles.
*/
class Profiler extends React.Component<ProfilerProps> {
// The activity representing how long it takes to mount a component.
public mountActivity: number | null = null;
// The span of the mount activity
public mountSpan: Span | undefined = undefined;
// The span of the render
Expand All @@ -116,18 +54,21 @@ class Profiler extends React.Component<ProfilerProps> {
return;
}

if (getTracingIntegration()) {
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();
}
}

public componentDidUpdate({ updateProps, includeUpdates = true }: ProfilerProps): void {
Expand Down Expand Up @@ -221,22 +162,27 @@ 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 Down
5 changes: 3 additions & 2 deletions packages/tracing/src/hubextensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { getMainCarrier, Hub } from '@sentry/hub';
import { SpanContext, TransactionContext } from '@sentry/types';
import { logger } from '@sentry/utils';

import { IdleTransaction } from './idletransaction';
import { Span } from './span';
import { Transaction } from './transaction';

Expand All @@ -22,8 +23,8 @@ function traceHeaders(this: Hub): { [key: string]: string } {
/**
* {@see Hub.startTransaction}
*/
function startTransaction(this: Hub, context: TransactionContext): Transaction {
const transaction = new Transaction(context, this);
function startTransaction(this: Hub, context: TransactionContext, idleTimeout?: number): Transaction {
const transaction = idleTimeout ? new IdleTransaction(context, this, idleTimeout) : new Transaction(context, this);

const client = this.getClient();
// Roll the dice for sampling transaction, all child spans inherit the sampling decision.
Expand Down
Loading