Skip to content
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
94a9e2e
fix(apm): Set sampled to true by default
HazAT Mar 18, 2020
a92c730
meta: Changelog
HazAT Mar 18, 2020
715bd45
fix(apm): Sampling
HazAT Mar 18, 2020
c82f1d8
ref: Remove set default op
HazAT Mar 18, 2020
7c8d803
fix: Sampling decision
HazAT Mar 18, 2020
895c188
ref: Add comment
HazAT Mar 18, 2020
b0870b6
ref: typo
HazAT Mar 18, 2020
a5b5f2b
ref: Changes to docblock
HazAT Mar 18, 2020
2c662dc
ref: Change message
HazAT Mar 18, 2020
b96e512
Apply suggestions from code review
HazAT Mar 18, 2020
4139da7
Update packages/types/src/options.ts
HazAT Mar 18, 2020
5a66e0b
Update packages/types/src/options.ts
HazAT Mar 18, 2020
450259e
ref: Remove deprecated parts
HazAT Mar 18, 2020
b29a8b4
Merge branch 'apm/set-sampled' of github.com:getsentry/sentry-javascr…
HazAT Mar 18, 2020
86ce10f
fix: Maxlen
HazAT Mar 18, 2020
a1b2496
ref: Rework when and how integrations are setup
HazAT Mar 18, 2020
ea581e7
ref(apm): Send a span if it's not a child
HazAT Mar 18, 2020
3583578
ref: Tracing integration
HazAT Mar 18, 2020
2bbd2e7
fix: tests
HazAT Mar 18, 2020
c3f2750
fix: Span / Transaction creation
HazAT Mar 18, 2020
de8fefa
ref: CodeReview
HazAT Mar 18, 2020
29f392e
fix: Setup integrations when after we bound a client to the hub
HazAT Mar 18, 2020
878a8fc
ref: CodeReview
HazAT Mar 18, 2020
a2733e6
fix: tests
HazAT Mar 18, 2020
74a9894
Update packages/types/src/span.ts
HazAT Mar 18, 2020
319fe10
Merge branch 'apm/set-sampled' of github.com:getsentry/sentry-javascr…
HazAT Mar 18, 2020
4332b35
ref: CodeReview
HazAT Mar 18, 2020
7a114f5
ref: CodeReview
HazAT Mar 18, 2020
a695cd9
fix: Tests
HazAT Mar 18, 2020
498e3ff
ref: Refactor SpanRecorder -> SpanList
HazAT Mar 19, 2020
26bee09
ref: Rename back to SpanRecorder to be consistent with Python
HazAT Mar 19, 2020
a2351ab
ref: SpanRecorder
HazAT Mar 19, 2020
0ad436e
ref: Remove makeRoot
HazAT Mar 19, 2020
6d471b9
ref: Changelog
HazAT Mar 19, 2020
106905d
meta: Changelog
HazAT Mar 19, 2020
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: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott

- [apm] fix: Sampling of traces (#2500)
- [apm] ref: Remove status from tags in transaction (#2497)
- [browser] fix: Respect breadcrumbs sentry:false option (#2499)

## 5.14.2

- [apm] fix: Use Performance API for timings when available, including Web Workers (#2492)
Expand Down
33 changes: 18 additions & 15 deletions packages/apm/src/hubextensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,39 +30,42 @@ function traceHeaders(): { [key: string]: string } {

/**
* This functions starts a span. If argument passed is of type `Span`, it'll run sampling on it if configured
* and attach a `SpanRecorder`. If it's of type `SpanContext` and there is already a `Span` on the Scope,
* and attach a `SpanList`. If it's of type `SpanContext` and there is already a `Span` on the Scope,
* the created Span will have a reference to it and become it's child. Otherwise it'll crete a new `Span`.
*
* @param span Already constructed span which should be started or properties with which the span should be created
* @param spanOrSpanContext Already constructed span or properties with which the span should be created
* @param makeRoot This will just create the span as it is and will not attach it to the span on the scope (if there is one).
* Under some circumstances, in internal integrations, for example, this is used to make sure they are not interfering with each other.
*/
function startSpan(spanOrSpanContext?: Span | SpanContext, forceNoChild: boolean = false): Span {
function startSpan(spanOrSpanContext?: Span | SpanContext, makeRoot: boolean = false): Span {
// @ts-ignore
const that = this as Hub;
const scope = that.getScope();
const client = that.getClient();
const hub = this as Hub;
const scope = hub.getScope();
const client = hub.getClient();
let span;

if (!isSpanInstance(spanOrSpanContext) && !forceNoChild) {
if (scope) {
const parentSpan = scope.getSpan() as Span;
if (parentSpan) {
span = parentSpan.child(spanOrSpanContext);
}
if (!isSpanInstance(spanOrSpanContext) && !makeRoot && scope) {
const parentSpan = scope.getSpan() as Span;
if (parentSpan) {
span = parentSpan.child(spanOrSpanContext);
}
}

if (!isSpanInstance(span)) {
span = new Span(spanOrSpanContext, that);
span = new Span(spanOrSpanContext, hub);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit funny, strange. If spanOrSpanContext is already a Span value, why do we construct a new Span here? Among other side-effects, it resets the span.startTimestamp.

Note that the Span.constructor is defined as:

  public constructor(spanContext?: SpanContext, hub?: Hub) {

And we pass a Span | SpanContext.

This comment was marked as outdated.


if (span.sampled === undefined && span.transaction !== undefined) {
// We only roll the dice on sampling for "root" spans (transactions) because the childs inherit this state
if (span.sampled === undefined && !span.isChildSpan()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion for readability:

if (span.isTransaction() && span.sampled === undefined) {

Then we can probably remove the comment from above, or say

// Decide whether this transaction should be sent to Sentry. Child spans inherit this value.

Which makes me think that if we had two different types, Span and Transaction, Span.sampled() would read from Transaction.sampled instead of keeping a value internally.

Just again thinking about misuse...

let t = new Span({transaction: "test"});
let child = t.child({sampled: true});      // this is semantically "wrong", but the value will be silently ignored

const sampleRate = (client && client.getOptions().tracesSampleRate) || 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The || 0 here hides a configuration error or the unavailability of client, and has bitten us.

tracesSampleRate is already defaulted when setting the client options, I think we should not do the || 0 here, wdyt?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, isn't it a programming error if client is undefined?

Shouldn't we be able to write simply:

Suggested change
const sampleRate = (client && client.getOptions().tracesSampleRate) || 0;
const sampleRate = client.getOptions().tracesSampleRate;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Client can be optional on the hub, that's why we need it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is no client, then sampleRate can only be 0, okay.
Wouldn't this be a case to log a debug message? We debug log when the noop transport is in use in the Go SDK => when one sees that in the debug log, it is obvious why events are not sent. Here it seems like a similar situation, if there is no client we can't ever send any event, the SDK is somewhat disabled.

span.sampled = Math.random() < sampleRate;
}

// We only want to create a span list if we sampled the transaction
// in case we will discard the span anyway because sampled == false, we safe memory and do not store child spans
if (span.sampled) {
const experimentsOptions = (client && client.getOptions()._experiments) || {};
span.initFinishedSpans(experimentsOptions.maxSpans as number);
span.initSpanList(experimentsOptions.maxSpans as number);
}

return span;
Expand Down
123 changes: 27 additions & 96 deletions packages/apm/src/integrations/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,6 @@ interface TracingOptions {
* Default: true
*/
startTransactionOnLocationChange: boolean;
/**
* Sample to determine if the Integration should instrument anything. The decision will be taken once per load
* on initalization.
* 0 = 0% chance of instrumenting
* 1 = 100% change of instrumenting
*
* Default: 1
*/
tracesSampleRate: number;

/**
* The maximum duration of a transaction before it will be discarded. This is for some edge cases where a browser
Expand Down Expand Up @@ -109,11 +100,6 @@ export class Tracing implements Integration {
*/
public static id: string = 'Tracing';

/**
* Is Tracing enabled, this will be determined once per pageload.
*/
private static _enabled?: boolean;

/** JSDoc */
public static options: TracingOptions;

Expand Down Expand Up @@ -163,7 +149,6 @@ export class Tracing implements Integration {
startTransactionOnLocationChange: true,
traceFetch: true,
traceXHR: true,
tracesSampleRate: 1,
tracingOrigins: defaultTracingOrigins,
};
// NOTE: Logger doesn't work in contructors, as it's initialized after integrations instances
Expand All @@ -189,16 +174,11 @@ export class Tracing implements Integration {
logger.warn(`[Tracing] We added a reasonable default for you: ${defaultTracingOrigins}`);
}

if (!Tracing._isEnabled()) {
return;
}

// Starting our inital pageload transaction
if (global.location && global.location.href) {
// `${global.location.href}` will be used a temp transaction name
Tracing.startIdleTransaction(global.location.href, {
op: 'pageload',
sampled: true,
});
}

Expand All @@ -221,17 +201,15 @@ export class Tracing implements Integration {
return event;
}

if (Tracing._isEnabled()) {
const isOutdatedTransaction =
event.timestamp &&
event.start_timestamp &&
(event.timestamp - event.start_timestamp > Tracing.options.maxTransactionDuration ||
event.timestamp - event.start_timestamp < 0);
const isOutdatedTransaction =
event.timestamp &&
event.start_timestamp &&
(event.timestamp - event.start_timestamp > Tracing.options.maxTransactionDuration ||
event.timestamp - event.start_timestamp < 0);

if (Tracing.options.maxTransactionDuration !== 0 && event.type === 'transaction' && isOutdatedTransaction) {
logger.log('[Tracing] Discarded transaction since it maxed out maxTransactionDuration');
return null;
}
if (Tracing.options.maxTransactionDuration !== 0 && event.type === 'transaction' && isOutdatedTransaction) {
logger.log('[Tracing] Discarded transaction since it maxed out maxTransactionDuration');
return null;
}

return event;
Expand Down Expand Up @@ -345,7 +323,7 @@ export class Tracing implements Integration {
* If an error or unhandled promise occurs, we mark the active transaction as failed
*/
logger.log(`[Tracing] Global error occured, setting status in transaction: ${SpanStatus.InternalError}`);
(Tracing._activeTransaction as SpanClass).setStatus(SpanStatus.InternalError);
Tracing._activeTransaction.setStatus(SpanStatus.InternalError);
}
}
addInstrumentationHandler({
Expand All @@ -358,31 +336,10 @@ export class Tracing implements Integration {
});
}

/**
* Is tracing enabled
*/
private static _isEnabled(): boolean {
if (Tracing._enabled !== undefined) {
return Tracing._enabled;
}
// This happens only in test cases where the integration isn't initalized properly
// tslint:disable-next-line: strict-type-predicates
if (!Tracing.options || typeof Tracing.options.tracesSampleRate !== 'number') {
return false;
}
Tracing._enabled = Math.random() > Tracing.options.tracesSampleRate ? false : true;
return Tracing._enabled;
}

/**
* Starts a Transaction waiting for activity idle to finish
*/
public static startIdleTransaction(name: string, spanContext?: SpanContext): Span | undefined {
if (!Tracing._isEnabled()) {
// Tracing is not enabled
return undefined;
}

// If we already have an active transaction it means one of two things
// a) The user did rapid navigation changes and didn't wait until the transaction was finished
// b) A activity wasn't popped correctly and therefore the transaction is stalling
Expand All @@ -400,47 +357,22 @@ export class Tracing implements Integration {
return undefined;
}

const span = hub.startSpan(
Tracing._activeTransaction = hub.startSpan(
{
...spanContext,
transaction: name,
},
true,
);

Tracing._activeTransaction = span;

// We need to do this workaround here and not use configureScope
// Reason being at the time we start the inital transaction we do not have a client bound on the hub yet
// therefore configureScope wouldn't be executed and we would miss setting the transaction
// tslint:disable-next-line: no-unsafe-any
(hub as any).getScope().setSpan(span);

// The reason we do this here is because of cached responses
// If we start and transaction without an activity it would never finish since there is no activity
const id = Tracing.pushActivity('idleTransactionStarted');
setTimeout(() => {
Tracing.popActivity(id);
}, (Tracing.options && Tracing.options.idleTimeout) || 100);

return span;
}

/**
* Update transaction
* @deprecated
*/
public static updateTransactionName(name: string): void {
logger.log('[Tracing] DEPRECATED, use Sentry.configureScope => scope.setTransaction instead', name);
const _getCurrentHub = Tracing._getCurrentHub;
if (_getCurrentHub) {
const hub = _getCurrentHub();
if (hub) {
hub.configureScope(scope => {
scope.setTransaction(name);
});
}
}
return Tracing._activeTransaction;
}

/**
Expand Down Expand Up @@ -475,8 +407,8 @@ export class Tracing implements Integration {

// tslint:disable-next-line: completed-docs
function addSpan(span: SpanClass): void {
if (transactionSpan.spanRecorder) {
transactionSpan.spanRecorder.finishSpan(span);
if (transactionSpan.spanList) {
transactionSpan.spanList.finishSpan(span);
}
}

Expand Down Expand Up @@ -566,8 +498,8 @@ export class Tracing implements Integration {
const resourceName = entry.name.replace(window.location.origin, '');
if (entry.initiatorType === 'xmlhttprequest' || entry.initiatorType === 'fetch') {
// We need to update existing spans with new timing info
if (transactionSpan.spanRecorder) {
transactionSpan.spanRecorder.finishedSpans.map((finishedSpan: SpanClass) => {
if (transactionSpan.spanList) {
transactionSpan.spanList.finishedSpans.map((finishedSpan: SpanClass) => {
if (finishedSpan.description && finishedSpan.description.indexOf(resourceName) !== -1) {
finishedSpan.startTimestamp = timeOrigin + startTime;
finishedSpan.timestamp = finishedSpan.startTimestamp + duration;
Expand Down Expand Up @@ -641,11 +573,9 @@ export class Tracing implements Integration {
autoPopAfter?: number;
},
): number {
if (!Tracing._isEnabled()) {
// Tracing is not enabled
return 0;
}
if (!Tracing._activeTransaction) {
const activeTransaction = Tracing._activeTransaction;

if (!activeTransaction) {
logger.log(`[Tracing] Not pushing activity ${name} since there is no active transaction`);
return 0;
}
Expand All @@ -657,7 +587,7 @@ export class Tracing implements Integration {
if (spanContext && _getCurrentHub) {
const hub = _getCurrentHub();
if (hub) {
const span = hub.startSpan(spanContext);
const span = activeTransaction.child(spanContext);
Tracing._activities[Tracing._currentIndex] = {
name,
span,
Expand Down Expand Up @@ -689,18 +619,16 @@ export class Tracing implements Integration {
*/
public static popActivity(id: number, spanData?: { [key: string]: any }): void {
// The !id is on purpose to also fail with 0
// Since 0 is returned by push activity in case tracing is not enabled
// or there is no active transaction
if (!Tracing._isEnabled() || !id) {
// Tracing is not enabled
// Since 0 is returned by push activity in case there is no active transaction
if (!id) {
return;
}

const activity = Tracing._activities[id];

if (activity) {
logger.log(`[Tracing] popActivity ${activity.name}#${id}`);
const span = activity.span as SpanClass;
const span = activity.span;
if (span) {
if (spanData) {
Object.keys(spanData).forEach((key: string) => {
Expand Down Expand Up @@ -778,7 +706,11 @@ function xhrCallback(handlerData: { [key: string]: any }): void {
if (activity) {
const span = activity.span;
if (span && handlerData.xhr.setRequestHeader) {
handlerData.xhr.setRequestHeader('sentry-trace', span.toTraceparent());
try {
handlerData.xhr.setRequestHeader('sentry-trace', span.toTraceparent());
} catch (_) {
// Error: InvalidStateError: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED.
}
}
}
// tslint:enable: no-unsafe-any
Expand Down Expand Up @@ -839,7 +771,6 @@ function historyCallback(_: { [key: string]: any }): void {
if (Tracing.options.startTransactionOnLocationChange && global && global.location) {
Tracing.startIdleTransaction(global.location.href, {
op: 'navigation',
sampled: true,
});
}
}
Loading