Merged
171 changes: 60 additions & 111 deletions packages/browser/src/profiling/hubextensions.ts
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,17 @@
import { getCurrentHub, getMainCarrier } from '@sentry/core';
import type { CustomSamplingContext, Hub, Transaction, TransactionContext } from '@sentry/types';
/* eslint-disable complexity */
import { getCurrentHub } from '@sentry/core';
import type { Transaction } from '@sentry/types';
import { logger, uuid4 } from '@sentry/utils';

import { WINDOW } from '../helpers';
import type {
JSSelfProfile,
JSSelfProfiler,
JSSelfProfilerConstructor,
ProcessedJSSelfProfile,
} from './jsSelfProfiling';
import { sendProfile } from './sendProfile';
import type { JSSelfProfile, JSSelfProfiler, JSSelfProfilerConstructor } from './jsSelfProfiling';
import { addProfileToMap, isValidSampleRate } from './utils';

// Max profile duration.
const MAX_PROFILE_DURATION_MS = 30_000;
export const MAX_PROFILE_DURATION_MS = 30_000;
// Keep a flag value to avoid re-initializing the profiler constructor. If it fails
// once, it will always fail and this allows us to early return.
let PROFILING_CONSTRUCTOR_FAILED = false;

// While we experiment, per transaction sampling interval will be more flexible to work with.
type StartTransaction = (
this: Hub,
transactionContext: TransactionContext,
customSamplingContext?: CustomSamplingContext,
) => Transaction | undefined;

/**
* Check if profiler constructor is available.
* @param maybeProfiler
Expand DownExpand Up@@ -55,7 +43,7 @@ export function onProfilingStartRouteTransaction(transaction: Transaction | unde
* startProfiling is called after the call to startTransaction in order to avoid our own code from
* being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction.
*/
function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
export function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
// Feature support check first
const JSProfilerConstructor = WINDOW.Profiler;

Expand All@@ -68,14 +56,6 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
return transaction;
}

// profilesSampleRate is multiplied with tracesSampleRate to get the final sampling rate.
if (!transaction.sampled) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Transaction is not sampled, skipping profiling');
}
return transaction;
}

// If constructor failed once, it will always fail, so we can early return.
if (PROFILING_CONSTRUCTOR_FAILED) {
if (__DEBUG_BUILD__) {
Expand All@@ -86,21 +66,41 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {

const client = getCurrentHub().getClient();
const options = client && client.getOptions();
if (!options) {
__DEBUG_BUILD__ && logger.log('[Profiling] Profiling disabled, no options found.');
return transaction;
}

// @ts-ignore not part of the browser options yet
const profilesSampleRate = (options && options.profilesSampleRate) || 0;
if (profilesSampleRate === undefined) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Profiling disabled, enable it by setting `profilesSampleRate` option to SDK init call.');
}
// @ts-ignore profilesSampleRate is not part of the browser options yet
const profilesSampleRate: number | boolean | undefined = options.profilesSampleRate;

// Since this is coming from the user (or from a function provided by the user), who knows what we might get. (The
// only valid values are booleans or numbers between 0 and 1.)
if (!isValidSampleRate(profilesSampleRate)) {
__DEBUG_BUILD__ && logger.warn('[Profiling] Discarding profile because of invalid sample rate.');
return transaction;
}

// if the function returned 0 (or false), or if `profileSampleRate` is 0, it's a sign the profile should be dropped
if (!profilesSampleRate) {
__DEBUG_BUILD__ &&
logger.log(
'[Profiling] Discarding profile because a negative sampling decision was inherited or profileSampleRate is set to 0',
);
return transaction;
}

// Now we roll the dice. Math.random is inclusive of 0, but not of 1, so strict < is safe here. In case sampleRate is
// a boolean, the < comparison will cause it to be automatically cast to 1 if it's true and 0 if it's false.
const sampled = profilesSampleRate === true ? true : Math.random() < profilesSampleRate;
// Check if we should sample this profile
if (Math.random() > profilesSampleRate) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Skip profiling transaction due to sampling.');
}
if (!sampled) {
__DEBUG_BUILD__ &&
logger.log(
`[Profiling] Discarding profile because it's not included in the random sample (sampling rate = ${Number(
profilesSampleRate,
)})`,
);
return transaction;
}

Expand DownExpand Up@@ -147,19 +147,19 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
// event of an error or user mistake (calling transaction.finish multiple times), it is important that the behavior of onProfileHandler
// is idempotent as we do not want any timings or profiles to be overriden by the last call to onProfileHandler.
// After the original finish method is called, the event will be reported through the integration and delegated to transport.
let processedProfile: ProcessedJSSelfProfile | null = null;
const processedProfile: JSSelfProfile | null = null;

/**
* Idempotent handler for profile stop
*/
function onProfileHandler(): void {
async function onProfileHandler(): Promise<null> {
// Check if the profile exists and return it the behavior has to be idempotent as users may call transaction.finish multiple times.
if (!transaction) {
return;
return null;
}
// Satisfy the type checker, but profiler will always be defined here.
if (!profiler) {
return;
return null;
}
if (processedProfile) {
if (__DEBUG_BUILD__) {
Expand All@@ -169,12 +169,12 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
'already exists, returning early',
);
}
return;
return null;
}

profiler
return profiler
.stop()
.then((p: JSSelfProfile): void => {
.then((p: JSSelfProfile): null => {
if (maxDurationTimeoutID) {
WINDOW.clearTimeout(maxDurationTimeoutID);
maxDurationTimeoutID = undefined;
Expand All@@ -192,16 +192,11 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
'this may indicate an overlapping transaction or a call to stopProfiling with a profile title that was never started',
);
}
return;
}

// If a profile has less than 2 samples, it is not useful and should be discarded.
if (p.samples.length < 2) {
return;
return null;
}

processedProfile = { ...p, profile_id: profileId };
sendProfile(profileId, processedProfile);
addProfileToMap(profileId, p);
return null;
})
.catch(error => {
if (__DEBUG_BUILD__) {
Expand All@@ -219,6 +214,7 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
transaction.name || transaction.description,
);
}
// If the timeout exceeds, we want to stop profiling, but not finish the transaction
void onProfileHandler();
}, MAX_PROFILE_DURATION_MS);

Expand All@@ -230,73 +226,26 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
* startProfiling is called after the call to startTransaction in order to avoid our own code from
* being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction.
*/
function profilingWrappedTransactionFinish(): Promise<Transaction> {
function profilingWrappedTransactionFinish(): Transaction {
if (!transaction) {
return originalFinish();
}
// onProfileHandler should always return the same profile even if this is called multiple times.
// Always call onProfileHandler to ensure stopProfiling is called and the timeout is cleared.
onProfileHandler();

// Set profile context
transaction.setContext('profile', { profile_id: profileId });
void onProfileHandler().then(
() => {
transaction.setContext('profile', { profile_id: profileId });
originalFinish();
},
() => {
// If onProfileHandler fails, we still want to call the original finish method.
originalFinish();
},
);

return originalFinish();
return transaction;
}

transaction.finish = profilingWrappedTransactionFinish;
return transaction;
}

/**
* Wraps startTransaction with profiling logic. This is done automatically by the profiling integration.
*/
function __PRIVATE__wrapStartTransactionWithProfiling(startTransaction: StartTransaction): StartTransaction {
return function wrappedStartTransaction(
this: Hub,
transactionContext: TransactionContext,
customSamplingContext?: CustomSamplingContext,
): Transaction | undefined {
const transaction: Transaction | undefined = startTransaction.call(this, transactionContext, customSamplingContext);
if (transaction === undefined) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Transaction is undefined, skipping profiling');
}
return transaction;
}

return wrapTransactionWithProfiling(transaction);
};
}

/**
* Patches startTransaction and stopTransaction with profiling logic.
*/
export function addProfilingExtensionMethods(): void {
const carrier = getMainCarrier();
if (!carrier.__SENTRY__) {
if (__DEBUG_BUILD__) {
logger.log("[Profiling] Can't find main carrier, profiling won't work.");
}
return;
}
carrier.__SENTRY__.extensions = carrier.__SENTRY__.extensions || {};

if (!carrier.__SENTRY__.extensions['startTransaction']) {
if (__DEBUG_BUILD__) {
logger.log(
'[Profiling] startTransaction does not exists, profiling will not work. Make sure you import @sentry/tracing package before @sentry/profiling-node as import order matters.',
);
}
return;
}

if (__DEBUG_BUILD__) {
logger.log('[Profiling] startTransaction exists, patching it with profiling functionality...');
}

carrier.__SENTRY__.extensions['startTransaction'] = __PRIVATE__wrapStartTransactionWithProfiling(
// This is already patched by sentry/tracing, we are going to re-patch it...
carrier.__SENTRY__.extensions['startTransaction'] as StartTransaction,
);
}
92 changes: 66 additions & 26 deletions packages/browser/src/profiling/integration.ts
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
import type { Event, EventProcessor, Integration } from '@sentry/types';
import type { EventProcessor, Hub, Integration, Transaction } from '@sentry/types';
import type { Profile } from '@sentry/types/src/profiling';
import { logger } from '@sentry/utils';

import { PROFILING_EVENT_CACHE } from './cache';
import { addProfilingExtensionMethods } from './hubextensions';
import type { BrowserClient } from './../client';
import { wrapTransactionWithProfiling } from './hubextensions';
import type { ProfiledEvent } from './utils';
import {
addProfilesToEnvelope,
createProfilingEvent,
findProfiledTransactionsFromEnvelope,
PROFILE_MAP,
} from './utils';

/**
* Browser profiling integration. Stores any event that has contexts["profile"]["profile_id"]
Expand All@@ -15,34 +23,66 @@ import { addProfilingExtensionMethods } from './hubextensions';
*/
export class BrowserProfilingIntegration implements Integration {
public readonly name: string = 'BrowserProfilingIntegration';
public getCurrentHub?: () => Hub = undefined;

/**
* @inheritDoc
*/
public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void): void {
// Patching the hub to add the extension methods.
// Warning: we have an implicit dependency on import order and we will fail patching if the constructor of
// BrowserProfilingIntegration is called before @sentry/tracing is imported. This is because we need to patch
// the methods of @sentry/tracing which are patched as a side effect of importing @sentry/tracing.
addProfilingExtensionMethods();

// Add our event processor
addGlobalEventProcessor(this.handleGlobalEvent.bind(this));
}
public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void {
this.getCurrentHub = getCurrentHub;
const client = this.getCurrentHub().getClient() as BrowserClient;

/**
* @inheritDoc
*/
public handleGlobalEvent(event: Event): Event {
const profileId = event.contexts && event.contexts['profile'] && event.contexts['profile']['profile_id'];

if (profileId && typeof profileId === 'string') {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Profiling event found, caching it.');
}
PROFILING_EVENT_CACHE.add(profileId, event);
}
if (client && typeof client.on === 'function') {
client.on('startTransaction', (transaction: Transaction) => {
wrapTransactionWithProfiling(transaction);
});

client.on('beforeEnvelope', (envelope): void => {
// if not profiles are in queue, there is nothing to add to the envelope.
if (!PROFILE_MAP['size']) {
return;
}

const profiledTransactionEvents = findProfiledTransactionsFromEnvelope(envelope);
if (!profiledTransactionEvents.length) {
return;
}

const profilesToAddToEnvelope: Profile[] = [];

return event;
for (const profiledTransaction of profiledTransactionEvents) {
const context = profiledTransaction && profiledTransaction.contexts;
const profile_id = context && context['profile'] && (context['profile']['profile_id'] as string);

if (!profile_id) {
__DEBUG_BUILD__ &&
logger.log('[Profiling] cannot find profile for a transaction without a profile context');
continue;
}

// Remove the profile from the transaction context before sending, relay will take care of the rest.
if (context && context['profile']) {
delete context.profile;
}

const profile = PROFILE_MAP.get(profile_id);
if (!profile) {
__DEBUG_BUILD__ && logger.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`);
continue;
}

PROFILE_MAP.delete(profile_id);
const profileEvent = createProfilingEvent(profile_id, profile, profiledTransaction as ProfiledEvent);

if (profileEvent) {
profilesToAddToEnvelope.push(profileEvent);
}
}

addProfilesToEnvelope(envelope, profilesToAddToEnvelope);
});
} else {
logger.warn('[Profiling] Client does not support hooks, profiling will be disabled');
}
}
}
8 changes: 0 additions & 8 deletions packages/browser/src/profiling/jsSelfProfiling.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,10 +26,6 @@ export type JSSelfProfile = {
samples: JSSelfProfileSample[];
};

export interface ProcessedJSSelfProfile extends JSSelfProfile {
profile_id: string;
}

type BufferFullCallback = (trace: JSSelfProfile) => void;

export interface JSSelfProfiler {
Expand All@@ -49,7 +45,3 @@ declare global {
Profiler: typeof JSSelfProfilerConstructor | undefined;
}
}

export interface RawThreadCpuProfile extends JSSelfProfile {
profile_id: string;
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
171 changes: 60 additions & 111 deletions packages/browser/src/profiling/hubextensions.ts
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,17 @@
import { getCurrentHub, getMainCarrier } from '@sentry/core';
import type { CustomSamplingContext, Hub, Transaction, TransactionContext } from '@sentry/types';
/* eslint-disable complexity */
import { getCurrentHub } from '@sentry/core';
import type { Transaction } from '@sentry/types';
import { logger, uuid4 } from '@sentry/utils';

import { WINDOW } from '../helpers';
import type {
JSSelfProfile,
JSSelfProfiler,
JSSelfProfilerConstructor,
ProcessedJSSelfProfile,
} from './jsSelfProfiling';
import { sendProfile } from './sendProfile';
import type { JSSelfProfile, JSSelfProfiler, JSSelfProfilerConstructor } from './jsSelfProfiling';
import { addProfileToMap, isValidSampleRate } from './utils';

// Max profile duration.
const MAX_PROFILE_DURATION_MS = 30_000;
export const MAX_PROFILE_DURATION_MS = 30_000;
// Keep a flag value to avoid re-initializing the profiler constructor. If it fails
// once, it will always fail and this allows us to early return.
let PROFILING_CONSTRUCTOR_FAILED = false;

// While we experiment, per transaction sampling interval will be more flexible to work with.
type StartTransaction = (
this: Hub,
transactionContext: TransactionContext,
customSamplingContext?: CustomSamplingContext,
) => Transaction | undefined;

/**
* Check if profiler constructor is available.
* @param maybeProfiler
Expand DownExpand Up@@ -55,7 +43,7 @@ export function onProfilingStartRouteTransaction(transaction: Transaction | unde
* startProfiling is called after the call to startTransaction in order to avoid our own code from
* being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction.
*/
function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
export function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
// Feature support check first
const JSProfilerConstructor = WINDOW.Profiler;

Expand All@@ -68,14 +56,6 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
return transaction;
}

// profilesSampleRate is multiplied with tracesSampleRate to get the final sampling rate.
if (!transaction.sampled) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Transaction is not sampled, skipping profiling');
}
return transaction;
}

// If constructor failed once, it will always fail, so we can early return.
if (PROFILING_CONSTRUCTOR_FAILED) {
if (__DEBUG_BUILD__) {
Expand All@@ -86,21 +66,41 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {

const client = getCurrentHub().getClient();
const options = client && client.getOptions();
if (!options) {
__DEBUG_BUILD__ && logger.log('[Profiling] Profiling disabled, no options found.');
return transaction;
}

// @ts-ignore not part of the browser options yet
const profilesSampleRate = (options && options.profilesSampleRate) || 0;
if (profilesSampleRate === undefined) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Profiling disabled, enable it by setting `profilesSampleRate` option to SDK init call.');
}
// @ts-ignore profilesSampleRate is not part of the browser options yet
const profilesSampleRate: number | boolean | undefined = options.profilesSampleRate;

// Since this is coming from the user (or from a function provided by the user), who knows what we might get. (The
// only valid values are booleans or numbers between 0 and 1.)
if (!isValidSampleRate(profilesSampleRate)) {
__DEBUG_BUILD__ && logger.warn('[Profiling] Discarding profile because of invalid sample rate.');
return transaction;
}

// if the function returned 0 (or false), or if `profileSampleRate` is 0, it's a sign the profile should be dropped
if (!profilesSampleRate) {
__DEBUG_BUILD__ &&
logger.log(
'[Profiling] Discarding profile because a negative sampling decision was inherited or profileSampleRate is set to 0',
);
return transaction;
}

// Now we roll the dice. Math.random is inclusive of 0, but not of 1, so strict < is safe here. In case sampleRate is
// a boolean, the < comparison will cause it to be automatically cast to 1 if it's true and 0 if it's false.
const sampled = profilesSampleRate === true ? true : Math.random() < profilesSampleRate;
// Check if we should sample this profile
if (Math.random() > profilesSampleRate) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Skip profiling transaction due to sampling.');
}
if (!sampled) {
__DEBUG_BUILD__ &&
logger.log(
`[Profiling] Discarding profile because it's not included in the random sample (sampling rate = ${Number(
profilesSampleRate,
)})`,
);
return transaction;
}

Expand DownExpand Up@@ -147,19 +147,19 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
// event of an error or user mistake (calling transaction.finish multiple times), it is important that the behavior of onProfileHandler
// is idempotent as we do not want any timings or profiles to be overriden by the last call to onProfileHandler.
// After the original finish method is called, the event will be reported through the integration and delegated to transport.
let processedProfile: ProcessedJSSelfProfile | null = null;
const processedProfile: JSSelfProfile | null = null;

/**
* Idempotent handler for profile stop
*/
function onProfileHandler(): void {
async function onProfileHandler(): Promise<null> {
// Check if the profile exists and return it the behavior has to be idempotent as users may call transaction.finish multiple times.
if (!transaction) {
return;
return null;
}
// Satisfy the type checker, but profiler will always be defined here.
if (!profiler) {
return;
return null;
}
if (processedProfile) {
if (__DEBUG_BUILD__) {
Expand All@@ -169,12 +169,12 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
'already exists, returning early',
);
}
return;
return null;
}

profiler
return profiler
.stop()
.then((p: JSSelfProfile): void => {
.then((p: JSSelfProfile): null => {
if (maxDurationTimeoutID) {
WINDOW.clearTimeout(maxDurationTimeoutID);
maxDurationTimeoutID = undefined;
Expand All@@ -192,16 +192,11 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
'this may indicate an overlapping transaction or a call to stopProfiling with a profile title that was never started',
);
}
return;
}

// If a profile has less than 2 samples, it is not useful and should be discarded.
if (p.samples.length < 2) {
return;
return null;
}

processedProfile = { ...p, profile_id: profileId };
sendProfile(profileId, processedProfile);
addProfileToMap(profileId, p);
return null;
})
.catch(error => {
if (__DEBUG_BUILD__) {
Expand All@@ -219,6 +214,7 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
transaction.name || transaction.description,
);
}
// If the timeout exceeds, we want to stop profiling, but not finish the transaction
void onProfileHandler();
}, MAX_PROFILE_DURATION_MS);

Expand All@@ -230,73 +226,26 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
* startProfiling is called after the call to startTransaction in order to avoid our own code from
* being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction.
*/
function profilingWrappedTransactionFinish(): Promise<Transaction> {
function profilingWrappedTransactionFinish(): Transaction {
if (!transaction) {
return originalFinish();
}
// onProfileHandler should always return the same profile even if this is called multiple times.
// Always call onProfileHandler to ensure stopProfiling is called and the timeout is cleared.
onProfileHandler();

// Set profile context
transaction.setContext('profile', { profile_id: profileId });
void onProfileHandler().then(
() => {
transaction.setContext('profile', { profile_id: profileId });
originalFinish();
},
() => {
// If onProfileHandler fails, we still want to call the original finish method.
originalFinish();
},
);

return originalFinish();
return transaction;
}

transaction.finish = profilingWrappedTransactionFinish;
return transaction;
}

/**
* Wraps startTransaction with profiling logic. This is done automatically by the profiling integration.
*/
function __PRIVATE__wrapStartTransactionWithProfiling(startTransaction: StartTransaction): StartTransaction {
return function wrappedStartTransaction(
this: Hub,
transactionContext: TransactionContext,
customSamplingContext?: CustomSamplingContext,
): Transaction | undefined {
const transaction: Transaction | undefined = startTransaction.call(this, transactionContext, customSamplingContext);
if (transaction === undefined) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Transaction is undefined, skipping profiling');
}
return transaction;
}

return wrapTransactionWithProfiling(transaction);
};
}

/**
* Patches startTransaction and stopTransaction with profiling logic.
*/
export function addProfilingExtensionMethods(): void {
const carrier = getMainCarrier();
if (!carrier.__SENTRY__) {
if (__DEBUG_BUILD__) {
logger.log("[Profiling] Can't find main carrier, profiling won't work.");
}
return;
}
carrier.__SENTRY__.extensions = carrier.__SENTRY__.extensions || {};

if (!carrier.__SENTRY__.extensions['startTransaction']) {
if (__DEBUG_BUILD__) {
logger.log(
'[Profiling] startTransaction does not exists, profiling will not work. Make sure you import @sentry/tracing package before @sentry/profiling-node as import order matters.',
);
}
return;
}

if (__DEBUG_BUILD__) {
logger.log('[Profiling] startTransaction exists, patching it with profiling functionality...');
}

carrier.__SENTRY__.extensions['startTransaction'] = __PRIVATE__wrapStartTransactionWithProfiling(
// This is already patched by sentry/tracing, we are going to re-patch it...
carrier.__SENTRY__.extensions['startTransaction'] as StartTransaction,
);
}
92 changes: 66 additions & 26 deletions packages/browser/src/profiling/integration.ts
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
import type { Event, EventProcessor, Integration } from '@sentry/types';
import type { EventProcessor, Hub, Integration, Transaction } from '@sentry/types';
import type { Profile } from '@sentry/types/src/profiling';
import { logger } from '@sentry/utils';

import { PROFILING_EVENT_CACHE } from './cache';
import { addProfilingExtensionMethods } from './hubextensions';
import type { BrowserClient } from './../client';
import { wrapTransactionWithProfiling } from './hubextensions';
import type { ProfiledEvent } from './utils';
import {
addProfilesToEnvelope,
createProfilingEvent,
findProfiledTransactionsFromEnvelope,
PROFILE_MAP,
} from './utils';

/**
* Browser profiling integration. Stores any event that has contexts["profile"]["profile_id"]
Expand All@@ -15,34 +23,66 @@ import { addProfilingExtensionMethods } from './hubextensions';
*/
export class BrowserProfilingIntegration implements Integration {
public readonly name: string = 'BrowserProfilingIntegration';
public getCurrentHub?: () => Hub = undefined;

/**
* @inheritDoc
*/
public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void): void {
// Patching the hub to add the extension methods.
// Warning: we have an implicit dependency on import order and we will fail patching if the constructor of
// BrowserProfilingIntegration is called before @sentry/tracing is imported. This is because we need to patch
// the methods of @sentry/tracing which are patched as a side effect of importing @sentry/tracing.
addProfilingExtensionMethods();

// Add our event processor
addGlobalEventProcessor(this.handleGlobalEvent.bind(this));
}
public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void {
this.getCurrentHub = getCurrentHub;
const client = this.getCurrentHub().getClient() as BrowserClient;

/**
* @inheritDoc
*/
public handleGlobalEvent(event: Event): Event {
const profileId = event.contexts && event.contexts['profile'] && event.contexts['profile']['profile_id'];

if (profileId && typeof profileId === 'string') {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Profiling event found, caching it.');
}
PROFILING_EVENT_CACHE.add(profileId, event);
}
if (client && typeof client.on === 'function') {
client.on('startTransaction', (transaction: Transaction) => {
wrapTransactionWithProfiling(transaction);
});

client.on('beforeEnvelope', (envelope): void => {
// if not profiles are in queue, there is nothing to add to the envelope.
if (!PROFILE_MAP['size']) {
return;
}

const profiledTransactionEvents = findProfiledTransactionsFromEnvelope(envelope);
if (!profiledTransactionEvents.length) {
return;
}

const profilesToAddToEnvelope: Profile[] = [];

return event;
for (const profiledTransaction of profiledTransactionEvents) {
const context = profiledTransaction && profiledTransaction.contexts;
const profile_id = context && context['profile'] && (context['profile']['profile_id'] as string);

if (!profile_id) {
__DEBUG_BUILD__ &&
logger.log('[Profiling] cannot find profile for a transaction without a profile context');
continue;
}

// Remove the profile from the transaction context before sending, relay will take care of the rest.
if (context && context['profile']) {
delete context.profile;
}

const profile = PROFILE_MAP.get(profile_id);
if (!profile) {
__DEBUG_BUILD__ && logger.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`);
continue;
}

PROFILE_MAP.delete(profile_id);
const profileEvent = createProfilingEvent(profile_id, profile, profiledTransaction as ProfiledEvent);

if (profileEvent) {
profilesToAddToEnvelope.push(profileEvent);
}
}

addProfilesToEnvelope(envelope, profilesToAddToEnvelope);
});
} else {
logger.warn('[Profiling] Client does not support hooks, profiling will be disabled');
}
}
}
8 changes: 0 additions & 8 deletions packages/browser/src/profiling/jsSelfProfiling.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,10 +26,6 @@ export type JSSelfProfile = {
samples: JSSelfProfileSample[];
};

export interface ProcessedJSSelfProfile extends JSSelfProfile {
profile_id: string;
}

type BufferFullCallback = (trace: JSSelfProfile) => void;

export interface JSSelfProfiler {
Expand All@@ -49,7 +45,3 @@ declare global {
Profiler: typeof JSSelfProfilerConstructor | undefined;
}
}

export interface RawThreadCpuProfile extends JSSelfProfile {
profile_id: string;
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
171 changes: 60 additions & 111 deletions packages/browser/src/profiling/hubextensions.ts
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,17 @@
import { getCurrentHub, getMainCarrier } from '@sentry/core';
import type { CustomSamplingContext, Hub, Transaction, TransactionContext } from '@sentry/types';
/* eslint-disable complexity */
import { getCurrentHub } from '@sentry/core';
import type { Transaction } from '@sentry/types';
import { logger, uuid4 } from '@sentry/utils';

import { WINDOW } from '../helpers';
import type {
JSSelfProfile,
JSSelfProfiler,
JSSelfProfilerConstructor,
ProcessedJSSelfProfile,
} from './jsSelfProfiling';
import { sendProfile } from './sendProfile';
import type { JSSelfProfile, JSSelfProfiler, JSSelfProfilerConstructor } from './jsSelfProfiling';
import { addProfileToMap, isValidSampleRate } from './utils';

// Max profile duration.
const MAX_PROFILE_DURATION_MS = 30_000;
export const MAX_PROFILE_DURATION_MS = 30_000;
// Keep a flag value to avoid re-initializing the profiler constructor. If it fails
// once, it will always fail and this allows us to early return.
let PROFILING_CONSTRUCTOR_FAILED = false;

// While we experiment, per transaction sampling interval will be more flexible to work with.
type StartTransaction = (
this: Hub,
transactionContext: TransactionContext,
customSamplingContext?: CustomSamplingContext,
) => Transaction | undefined;

/**
* Check if profiler constructor is available.
* @param maybeProfiler
Expand DownExpand Up@@ -55,7 +43,7 @@ export function onProfilingStartRouteTransaction(transaction: Transaction | unde
* startProfiling is called after the call to startTransaction in order to avoid our own code from
* being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction.
*/
function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
export function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
// Feature support check first
const JSProfilerConstructor = WINDOW.Profiler;

Expand All@@ -68,14 +56,6 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
return transaction;
}

// profilesSampleRate is multiplied with tracesSampleRate to get the final sampling rate.
if (!transaction.sampled) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Transaction is not sampled, skipping profiling');
}
return transaction;
}

// If constructor failed once, it will always fail, so we can early return.
if (PROFILING_CONSTRUCTOR_FAILED) {
if (__DEBUG_BUILD__) {
Expand All@@ -86,21 +66,41 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {

const client = getCurrentHub().getClient();
const options = client && client.getOptions();
if (!options) {
__DEBUG_BUILD__ && logger.log('[Profiling] Profiling disabled, no options found.');
return transaction;
}

// @ts-ignore not part of the browser options yet
const profilesSampleRate = (options && options.profilesSampleRate) || 0;
if (profilesSampleRate === undefined) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Profiling disabled, enable it by setting `profilesSampleRate` option to SDK init call.');
}
// @ts-ignore profilesSampleRate is not part of the browser options yet
const profilesSampleRate: number | boolean | undefined = options.profilesSampleRate;

// Since this is coming from the user (or from a function provided by the user), who knows what we might get. (The
// only valid values are booleans or numbers between 0 and 1.)
if (!isValidSampleRate(profilesSampleRate)) {
__DEBUG_BUILD__ && logger.warn('[Profiling] Discarding profile because of invalid sample rate.');
return transaction;
}

// if the function returned 0 (or false), or if `profileSampleRate` is 0, it's a sign the profile should be dropped
if (!profilesSampleRate) {
__DEBUG_BUILD__ &&
logger.log(
'[Profiling] Discarding profile because a negative sampling decision was inherited or profileSampleRate is set to 0',
);
return transaction;
}

// Now we roll the dice. Math.random is inclusive of 0, but not of 1, so strict < is safe here. In case sampleRate is
// a boolean, the < comparison will cause it to be automatically cast to 1 if it's true and 0 if it's false.
const sampled = profilesSampleRate === true ? true : Math.random() < profilesSampleRate;
// Check if we should sample this profile
if (Math.random() > profilesSampleRate) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Skip profiling transaction due to sampling.');
}
if (!sampled) {
__DEBUG_BUILD__ &&
logger.log(
`[Profiling] Discarding profile because it's not included in the random sample (sampling rate = ${Number(
profilesSampleRate,
)})`,
);
return transaction;
}

Expand DownExpand Up@@ -147,19 +147,19 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
// event of an error or user mistake (calling transaction.finish multiple times), it is important that the behavior of onProfileHandler
// is idempotent as we do not want any timings or profiles to be overriden by the last call to onProfileHandler.
// After the original finish method is called, the event will be reported through the integration and delegated to transport.
let processedProfile: ProcessedJSSelfProfile | null = null;
const processedProfile: JSSelfProfile | null = null;

/**
* Idempotent handler for profile stop
*/
function onProfileHandler(): void {
async function onProfileHandler(): Promise<null> {
// Check if the profile exists and return it the behavior has to be idempotent as users may call transaction.finish multiple times.
if (!transaction) {
return;
return null;
}
// Satisfy the type checker, but profiler will always be defined here.
if (!profiler) {
return;
return null;
}
if (processedProfile) {
if (__DEBUG_BUILD__) {
Expand All@@ -169,12 +169,12 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
'already exists, returning early',
);
}
return;
return null;
}

profiler
return profiler
.stop()
.then((p: JSSelfProfile): void => {
.then((p: JSSelfProfile): null => {
if (maxDurationTimeoutID) {
WINDOW.clearTimeout(maxDurationTimeoutID);
maxDurationTimeoutID = undefined;
Expand All@@ -192,16 +192,11 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
'this may indicate an overlapping transaction or a call to stopProfiling with a profile title that was never started',
);
}
return;
}

// If a profile has less than 2 samples, it is not useful and should be discarded.
if (p.samples.length < 2) {
return;
return null;
}

processedProfile = { ...p, profile_id: profileId };
sendProfile(profileId, processedProfile);
addProfileToMap(profileId, p);
return null;
})
.catch(error => {
if (__DEBUG_BUILD__) {
Expand All@@ -219,6 +214,7 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
transaction.name || transaction.description,
);
}
// If the timeout exceeds, we want to stop profiling, but not finish the transaction
void onProfileHandler();
}, MAX_PROFILE_DURATION_MS);

Expand All@@ -230,73 +226,26 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
* startProfiling is called after the call to startTransaction in order to avoid our own code from
* being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction.
*/
function profilingWrappedTransactionFinish(): Promise<Transaction> {
function profilingWrappedTransactionFinish(): Transaction {
if (!transaction) {
return originalFinish();
}
// onProfileHandler should always return the same profile even if this is called multiple times.
// Always call onProfileHandler to ensure stopProfiling is called and the timeout is cleared.
onProfileHandler();

// Set profile context
transaction.setContext('profile', { profile_id: profileId });
void onProfileHandler().then(
() => {
transaction.setContext('profile', { profile_id: profileId });
originalFinish();
},
() => {
// If onProfileHandler fails, we still want to call the original finish method.
originalFinish();
},
);

return originalFinish();
return transaction;
}

transaction.finish = profilingWrappedTransactionFinish;
return transaction;
}

/**
* Wraps startTransaction with profiling logic. This is done automatically by the profiling integration.
*/
function __PRIVATE__wrapStartTransactionWithProfiling(startTransaction: StartTransaction): StartTransaction {
return function wrappedStartTransaction(
this: Hub,
transactionContext: TransactionContext,
customSamplingContext?: CustomSamplingContext,
): Transaction | undefined {
const transaction: Transaction | undefined = startTransaction.call(this, transactionContext, customSamplingContext);
if (transaction === undefined) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Transaction is undefined, skipping profiling');
}
return transaction;
}

return wrapTransactionWithProfiling(transaction);
};
}

/**
* Patches startTransaction and stopTransaction with profiling logic.
*/
export function addProfilingExtensionMethods(): void {
const carrier = getMainCarrier();
if (!carrier.__SENTRY__) {
if (__DEBUG_BUILD__) {
logger.log("[Profiling] Can't find main carrier, profiling won't work.");
}
return;
}
carrier.__SENTRY__.extensions = carrier.__SENTRY__.extensions || {};

if (!carrier.__SENTRY__.extensions['startTransaction']) {
if (__DEBUG_BUILD__) {
logger.log(
'[Profiling] startTransaction does not exists, profiling will not work. Make sure you import @sentry/tracing package before @sentry/profiling-node as import order matters.',
);
}
return;
}

if (__DEBUG_BUILD__) {
logger.log('[Profiling] startTransaction exists, patching it with profiling functionality...');
}

carrier.__SENTRY__.extensions['startTransaction'] = __PRIVATE__wrapStartTransactionWithProfiling(
// This is already patched by sentry/tracing, we are going to re-patch it...
carrier.__SENTRY__.extensions['startTransaction'] as StartTransaction,
);
}
92 changes: 66 additions & 26 deletions packages/browser/src/profiling/integration.ts
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
import type { Event, EventProcessor, Integration } from '@sentry/types';
import type { EventProcessor, Hub, Integration, Transaction } from '@sentry/types';
import type { Profile } from '@sentry/types/src/profiling';
import { logger } from '@sentry/utils';

import { PROFILING_EVENT_CACHE } from './cache';
import { addProfilingExtensionMethods } from './hubextensions';
import type { BrowserClient } from './../client';
import { wrapTransactionWithProfiling } from './hubextensions';
import type { ProfiledEvent } from './utils';
import {
addProfilesToEnvelope,
createProfilingEvent,
findProfiledTransactionsFromEnvelope,
PROFILE_MAP,
} from './utils';

/**
* Browser profiling integration. Stores any event that has contexts["profile"]["profile_id"]
Expand All@@ -15,34 +23,66 @@ import { addProfilingExtensionMethods } from './hubextensions';
*/
export class BrowserProfilingIntegration implements Integration {
public readonly name: string = 'BrowserProfilingIntegration';
public getCurrentHub?: () => Hub = undefined;

/**
* @inheritDoc
*/
public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void): void {
// Patching the hub to add the extension methods.
// Warning: we have an implicit dependency on import order and we will fail patching if the constructor of
// BrowserProfilingIntegration is called before @sentry/tracing is imported. This is because we need to patch
// the methods of @sentry/tracing which are patched as a side effect of importing @sentry/tracing.
addProfilingExtensionMethods();

// Add our event processor
addGlobalEventProcessor(this.handleGlobalEvent.bind(this));
}
public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void {
this.getCurrentHub = getCurrentHub;
const client = this.getCurrentHub().getClient() as BrowserClient;

/**
* @inheritDoc
*/
public handleGlobalEvent(event: Event): Event {
const profileId = event.contexts && event.contexts['profile'] && event.contexts['profile']['profile_id'];

if (profileId && typeof profileId === 'string') {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Profiling event found, caching it.');
}
PROFILING_EVENT_CACHE.add(profileId, event);
}
if (client && typeof client.on === 'function') {
client.on('startTransaction', (transaction: Transaction) => {
wrapTransactionWithProfiling(transaction);
});

client.on('beforeEnvelope', (envelope): void => {
// if not profiles are in queue, there is nothing to add to the envelope.
if (!PROFILE_MAP['size']) {
return;
}

const profiledTransactionEvents = findProfiledTransactionsFromEnvelope(envelope);
if (!profiledTransactionEvents.length) {
return;
}

const profilesToAddToEnvelope: Profile[] = [];

return event;
for (const profiledTransaction of profiledTransactionEvents) {
const context = profiledTransaction && profiledTransaction.contexts;
const profile_id = context && context['profile'] && (context['profile']['profile_id'] as string);

if (!profile_id) {
__DEBUG_BUILD__ &&
logger.log('[Profiling] cannot find profile for a transaction without a profile context');
continue;
}

// Remove the profile from the transaction context before sending, relay will take care of the rest.
if (context && context['profile']) {
delete context.profile;
}

const profile = PROFILE_MAP.get(profile_id);
if (!profile) {
__DEBUG_BUILD__ && logger.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`);
continue;
}

PROFILE_MAP.delete(profile_id);
const profileEvent = createProfilingEvent(profile_id, profile, profiledTransaction as ProfiledEvent);

if (profileEvent) {
profilesToAddToEnvelope.push(profileEvent);
}
}

addProfilesToEnvelope(envelope, profilesToAddToEnvelope);
});
} else {
logger.warn('[Profiling] Client does not support hooks, profiling will be disabled');
}
}
}
8 changes: 0 additions & 8 deletions packages/browser/src/profiling/jsSelfProfiling.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,10 +26,6 @@ export type JSSelfProfile = {
samples: JSSelfProfileSample[];
};

export interface ProcessedJSSelfProfile extends JSSelfProfile {
profile_id: string;
}

type BufferFullCallback = (trace: JSSelfProfile) => void;

export interface JSSelfProfiler {
Expand All@@ -49,7 +45,3 @@ declare global {
Profiler: typeof JSSelfProfilerConstructor | undefined;
}
}

export interface RawThreadCpuProfile extends JSSelfProfile {
profile_id: string;
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
171 changes: 60 additions & 111 deletions packages/browser/src/profiling/hubextensions.ts
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,17 @@
import { getCurrentHub, getMainCarrier } from '@sentry/core';
import type { CustomSamplingContext, Hub, Transaction, TransactionContext } from '@sentry/types';
/* eslint-disable complexity */
import { getCurrentHub } from '@sentry/core';
import type { Transaction } from '@sentry/types';
import { logger, uuid4 } from '@sentry/utils';

import { WINDOW } from '../helpers';
import type {
JSSelfProfile,
JSSelfProfiler,
JSSelfProfilerConstructor,
ProcessedJSSelfProfile,
} from './jsSelfProfiling';
import { sendProfile } from './sendProfile';
import type { JSSelfProfile, JSSelfProfiler, JSSelfProfilerConstructor } from './jsSelfProfiling';
import { addProfileToMap, isValidSampleRate } from './utils';

// Max profile duration.
const MAX_PROFILE_DURATION_MS = 30_000;
export const MAX_PROFILE_DURATION_MS = 30_000;
// Keep a flag value to avoid re-initializing the profiler constructor. If it fails
// once, it will always fail and this allows us to early return.
let PROFILING_CONSTRUCTOR_FAILED = false;

// While we experiment, per transaction sampling interval will be more flexible to work with.
type StartTransaction = (
this: Hub,
transactionContext: TransactionContext,
customSamplingContext?: CustomSamplingContext,
) => Transaction | undefined;

/**
* Check if profiler constructor is available.
* @param maybeProfiler
Expand DownExpand Up@@ -55,7 +43,7 @@ export function onProfilingStartRouteTransaction(transaction: Transaction | unde
* startProfiling is called after the call to startTransaction in order to avoid our own code from
* being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction.
*/
function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
export function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
// Feature support check first
const JSProfilerConstructor = WINDOW.Profiler;

Expand All@@ -68,14 +56,6 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
return transaction;
}

// profilesSampleRate is multiplied with tracesSampleRate to get the final sampling rate.
if (!transaction.sampled) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Transaction is not sampled, skipping profiling');
}
return transaction;
}

// If constructor failed once, it will always fail, so we can early return.
if (PROFILING_CONSTRUCTOR_FAILED) {
if (__DEBUG_BUILD__) {
Expand All@@ -86,21 +66,41 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {

const client = getCurrentHub().getClient();
const options = client && client.getOptions();
if (!options) {
__DEBUG_BUILD__ && logger.log('[Profiling] Profiling disabled, no options found.');
return transaction;
}

// @ts-ignore not part of the browser options yet
const profilesSampleRate = (options && options.profilesSampleRate) || 0;
if (profilesSampleRate === undefined) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Profiling disabled, enable it by setting `profilesSampleRate` option to SDK init call.');
}
// @ts-ignore profilesSampleRate is not part of the browser options yet
const profilesSampleRate: number | boolean | undefined = options.profilesSampleRate;

// Since this is coming from the user (or from a function provided by the user), who knows what we might get. (The
// only valid values are booleans or numbers between 0 and 1.)
if (!isValidSampleRate(profilesSampleRate)) {
__DEBUG_BUILD__ && logger.warn('[Profiling] Discarding profile because of invalid sample rate.');
return transaction;
}

// if the function returned 0 (or false), or if `profileSampleRate` is 0, it's a sign the profile should be dropped
if (!profilesSampleRate) {
__DEBUG_BUILD__ &&
logger.log(
'[Profiling] Discarding profile because a negative sampling decision was inherited or profileSampleRate is set to 0',
);
return transaction;
}

// Now we roll the dice. Math.random is inclusive of 0, but not of 1, so strict < is safe here. In case sampleRate is
// a boolean, the < comparison will cause it to be automatically cast to 1 if it's true and 0 if it's false.
const sampled = profilesSampleRate === true ? true : Math.random() < profilesSampleRate;
// Check if we should sample this profile
if (Math.random() > profilesSampleRate) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Skip profiling transaction due to sampling.');
}
if (!sampled) {
__DEBUG_BUILD__ &&
logger.log(
`[Profiling] Discarding profile because it's not included in the random sample (sampling rate = ${Number(
profilesSampleRate,
)})`,
);
return transaction;
}

Expand DownExpand Up@@ -147,19 +147,19 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
// event of an error or user mistake (calling transaction.finish multiple times), it is important that the behavior of onProfileHandler
// is idempotent as we do not want any timings or profiles to be overriden by the last call to onProfileHandler.
// After the original finish method is called, the event will be reported through the integration and delegated to transport.
let processedProfile: ProcessedJSSelfProfile | null = null;
const processedProfile: JSSelfProfile | null = null;

/**
* Idempotent handler for profile stop
*/
function onProfileHandler(): void {
async function onProfileHandler(): Promise<null> {
// Check if the profile exists and return it the behavior has to be idempotent as users may call transaction.finish multiple times.
if (!transaction) {
return;
return null;
}
// Satisfy the type checker, but profiler will always be defined here.
if (!profiler) {
return;
return null;
}
if (processedProfile) {
if (__DEBUG_BUILD__) {
Expand All@@ -169,12 +169,12 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
'already exists, returning early',
);
}
return;
return null;
}

profiler
return profiler
.stop()
.then((p: JSSelfProfile): void => {
.then((p: JSSelfProfile): null => {
if (maxDurationTimeoutID) {
WINDOW.clearTimeout(maxDurationTimeoutID);
maxDurationTimeoutID = undefined;
Expand All@@ -192,16 +192,11 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
'this may indicate an overlapping transaction or a call to stopProfiling with a profile title that was never started',
);
}
return;
}

// If a profile has less than 2 samples, it is not useful and should be discarded.
if (p.samples.length < 2) {
return;
return null;
}

processedProfile = { ...p, profile_id: profileId };
sendProfile(profileId, processedProfile);
addProfileToMap(profileId, p);
return null;
})
.catch(error => {
if (__DEBUG_BUILD__) {
Expand All@@ -219,6 +214,7 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
transaction.name || transaction.description,
);
}
// If the timeout exceeds, we want to stop profiling, but not finish the transaction
void onProfileHandler();
}, MAX_PROFILE_DURATION_MS);

Expand All@@ -230,73 +226,26 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
* startProfiling is called after the call to startTransaction in order to avoid our own code from
* being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction.
*/
function profilingWrappedTransactionFinish(): Promise<Transaction> {
function profilingWrappedTransactionFinish(): Transaction {
if (!transaction) {
return originalFinish();
}
// onProfileHandler should always return the same profile even if this is called multiple times.
// Always call onProfileHandler to ensure stopProfiling is called and the timeout is cleared.
onProfileHandler();

// Set profile context
transaction.setContext('profile', { profile_id: profileId });
void onProfileHandler().then(
() => {
transaction.setContext('profile', { profile_id: profileId });
originalFinish();
},
() => {
// If onProfileHandler fails, we still want to call the original finish method.
originalFinish();
},
);

return originalFinish();
return transaction;
}

transaction.finish = profilingWrappedTransactionFinish;
return transaction;
}

/**
* Wraps startTransaction with profiling logic. This is done automatically by the profiling integration.
*/
function __PRIVATE__wrapStartTransactionWithProfiling(startTransaction: StartTransaction): StartTransaction {
return function wrappedStartTransaction(
this: Hub,
transactionContext: TransactionContext,
customSamplingContext?: CustomSamplingContext,
): Transaction | undefined {
const transaction: Transaction | undefined = startTransaction.call(this, transactionContext, customSamplingContext);
if (transaction === undefined) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Transaction is undefined, skipping profiling');
}
return transaction;
}

return wrapTransactionWithProfiling(transaction);
};
}

/**
* Patches startTransaction and stopTransaction with profiling logic.
*/
export function addProfilingExtensionMethods(): void {
const carrier = getMainCarrier();
if (!carrier.__SENTRY__) {
if (__DEBUG_BUILD__) {
logger.log("[Profiling] Can't find main carrier, profiling won't work.");
}
return;
}
carrier.__SENTRY__.extensions = carrier.__SENTRY__.extensions || {};

if (!carrier.__SENTRY__.extensions['startTransaction']) {
if (__DEBUG_BUILD__) {
logger.log(
'[Profiling] startTransaction does not exists, profiling will not work. Make sure you import @sentry/tracing package before @sentry/profiling-node as import order matters.',
);
}
return;
}

if (__DEBUG_BUILD__) {
logger.log('[Profiling] startTransaction exists, patching it with profiling functionality...');
}

carrier.__SENTRY__.extensions['startTransaction'] = __PRIVATE__wrapStartTransactionWithProfiling(
// This is already patched by sentry/tracing, we are going to re-patch it...
carrier.__SENTRY__.extensions['startTransaction'] as StartTransaction,
);
}
92 changes: 66 additions & 26 deletions packages/browser/src/profiling/integration.ts
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
import type { Event, EventProcessor, Integration } from '@sentry/types';
import type { EventProcessor, Hub, Integration, Transaction } from '@sentry/types';
import type { Profile } from '@sentry/types/src/profiling';
import { logger } from '@sentry/utils';

import { PROFILING_EVENT_CACHE } from './cache';
import { addProfilingExtensionMethods } from './hubextensions';
import type { BrowserClient } from './../client';
import { wrapTransactionWithProfiling } from './hubextensions';
import type { ProfiledEvent } from './utils';
import {
addProfilesToEnvelope,
createProfilingEvent,
findProfiledTransactionsFromEnvelope,
PROFILE_MAP,
} from './utils';

/**
* Browser profiling integration. Stores any event that has contexts["profile"]["profile_id"]
Expand All@@ -15,34 +23,66 @@ import { addProfilingExtensionMethods } from './hubextensions';
*/
export class BrowserProfilingIntegration implements Integration {
public readonly name: string = 'BrowserProfilingIntegration';
public getCurrentHub?: () => Hub = undefined;

/**
* @inheritDoc
*/
public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void): void {
// Patching the hub to add the extension methods.
// Warning: we have an implicit dependency on import order and we will fail patching if the constructor of
// BrowserProfilingIntegration is called before @sentry/tracing is imported. This is because we need to patch
// the methods of @sentry/tracing which are patched as a side effect of importing @sentry/tracing.
addProfilingExtensionMethods();

// Add our event processor
addGlobalEventProcessor(this.handleGlobalEvent.bind(this));
}
public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void {
this.getCurrentHub = getCurrentHub;
const client = this.getCurrentHub().getClient() as BrowserClient;

/**
* @inheritDoc
*/
public handleGlobalEvent(event: Event): Event {
const profileId = event.contexts && event.contexts['profile'] && event.contexts['profile']['profile_id'];

if (profileId && typeof profileId === 'string') {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Profiling event found, caching it.');
}
PROFILING_EVENT_CACHE.add(profileId, event);
}
if (client && typeof client.on === 'function') {
client.on('startTransaction', (transaction: Transaction) => {
wrapTransactionWithProfiling(transaction);
});

client.on('beforeEnvelope', (envelope): void => {
// if not profiles are in queue, there is nothing to add to the envelope.
if (!PROFILE_MAP['size']) {
return;
}

const profiledTransactionEvents = findProfiledTransactionsFromEnvelope(envelope);
if (!profiledTransactionEvents.length) {
return;
}

const profilesToAddToEnvelope: Profile[] = [];

return event;
for (const profiledTransaction of profiledTransactionEvents) {
const context = profiledTransaction && profiledTransaction.contexts;
const profile_id = context && context['profile'] && (context['profile']['profile_id'] as string);

if (!profile_id) {
__DEBUG_BUILD__ &&
logger.log('[Profiling] cannot find profile for a transaction without a profile context');
continue;
}

// Remove the profile from the transaction context before sending, relay will take care of the rest.
if (context && context['profile']) {
delete context.profile;
}

const profile = PROFILE_MAP.get(profile_id);
if (!profile) {
__DEBUG_BUILD__ && logger.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`);
continue;
}

PROFILE_MAP.delete(profile_id);
const profileEvent = createProfilingEvent(profile_id, profile, profiledTransaction as ProfiledEvent);

if (profileEvent) {
profilesToAddToEnvelope.push(profileEvent);
}
}

addProfilesToEnvelope(envelope, profilesToAddToEnvelope);
});
} else {
logger.warn('[Profiling] Client does not support hooks, profiling will be disabled');
}
}
}
8 changes: 0 additions & 8 deletions packages/browser/src/profiling/jsSelfProfiling.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,10 +26,6 @@ export type JSSelfProfile = {
samples: JSSelfProfileSample[];
};

export interface ProcessedJSSelfProfile extends JSSelfProfile {
profile_id: string;
}

type BufferFullCallback = (trace: JSSelfProfile) => void;

export interface JSSelfProfiler {
Expand All@@ -49,7 +45,3 @@ declare global {
Profiler: typeof JSSelfProfilerConstructor | undefined;
}
}

export interface RawThreadCpuProfile extends JSSelfProfile {
profile_id: string;
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
171 changes: 60 additions & 111 deletions packages/browser/src/profiling/hubextensions.ts
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,17 @@
import { getCurrentHub, getMainCarrier } from '@sentry/core';
import type { CustomSamplingContext, Hub, Transaction, TransactionContext } from '@sentry/types';
/* eslint-disable complexity */
import { getCurrentHub } from '@sentry/core';
import type { Transaction } from '@sentry/types';
import { logger, uuid4 } from '@sentry/utils';

import { WINDOW } from '../helpers';
import type {
JSSelfProfile,
JSSelfProfiler,
JSSelfProfilerConstructor,
ProcessedJSSelfProfile,
} from './jsSelfProfiling';
import { sendProfile } from './sendProfile';
import type { JSSelfProfile, JSSelfProfiler, JSSelfProfilerConstructor } from './jsSelfProfiling';
import { addProfileToMap, isValidSampleRate } from './utils';

// Max profile duration.
const MAX_PROFILE_DURATION_MS = 30_000;
export const MAX_PROFILE_DURATION_MS = 30_000;
// Keep a flag value to avoid re-initializing the profiler constructor. If it fails
// once, it will always fail and this allows us to early return.
let PROFILING_CONSTRUCTOR_FAILED = false;

// While we experiment, per transaction sampling interval will be more flexible to work with.
type StartTransaction = (
this: Hub,
transactionContext: TransactionContext,
customSamplingContext?: CustomSamplingContext,
) => Transaction | undefined;

/**
* Check if profiler constructor is available.
* @param maybeProfiler
Expand DownExpand Up@@ -55,7 +43,7 @@ export function onProfilingStartRouteTransaction(transaction: Transaction | unde
* startProfiling is called after the call to startTransaction in order to avoid our own code from
* being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction.
*/
function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
export function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
// Feature support check first
const JSProfilerConstructor = WINDOW.Profiler;

Expand All@@ -68,14 +56,6 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
return transaction;
}

// profilesSampleRate is multiplied with tracesSampleRate to get the final sampling rate.
if (!transaction.sampled) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Transaction is not sampled, skipping profiling');
}
return transaction;
}

// If constructor failed once, it will always fail, so we can early return.
if (PROFILING_CONSTRUCTOR_FAILED) {
if (__DEBUG_BUILD__) {
Expand All@@ -86,21 +66,41 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {

const client = getCurrentHub().getClient();
const options = client && client.getOptions();
if (!options) {
__DEBUG_BUILD__ && logger.log('[Profiling] Profiling disabled, no options found.');
return transaction;
}

// @ts-ignore not part of the browser options yet
const profilesSampleRate = (options && options.profilesSampleRate) || 0;
if (profilesSampleRate === undefined) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Profiling disabled, enable it by setting `profilesSampleRate` option to SDK init call.');
}
// @ts-ignore profilesSampleRate is not part of the browser options yet
const profilesSampleRate: number | boolean | undefined = options.profilesSampleRate;

// Since this is coming from the user (or from a function provided by the user), who knows what we might get. (The
// only valid values are booleans or numbers between 0 and 1.)
if (!isValidSampleRate(profilesSampleRate)) {
__DEBUG_BUILD__ && logger.warn('[Profiling] Discarding profile because of invalid sample rate.');
return transaction;
}

// if the function returned 0 (or false), or if `profileSampleRate` is 0, it's a sign the profile should be dropped
if (!profilesSampleRate) {
__DEBUG_BUILD__ &&
logger.log(
'[Profiling] Discarding profile because a negative sampling decision was inherited or profileSampleRate is set to 0',
);
return transaction;
}

// Now we roll the dice. Math.random is inclusive of 0, but not of 1, so strict < is safe here. In case sampleRate is
// a boolean, the < comparison will cause it to be automatically cast to 1 if it's true and 0 if it's false.
const sampled = profilesSampleRate === true ? true : Math.random() < profilesSampleRate;
// Check if we should sample this profile
if (Math.random() > profilesSampleRate) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Skip profiling transaction due to sampling.');
}
if (!sampled) {
__DEBUG_BUILD__ &&
logger.log(
`[Profiling] Discarding profile because it's not included in the random sample (sampling rate = ${Number(
profilesSampleRate,
)})`,
);
return transaction;
}

Expand DownExpand Up@@ -147,19 +147,19 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
// event of an error or user mistake (calling transaction.finish multiple times), it is important that the behavior of onProfileHandler
// is idempotent as we do not want any timings or profiles to be overriden by the last call to onProfileHandler.
// After the original finish method is called, the event will be reported through the integration and delegated to transport.
let processedProfile: ProcessedJSSelfProfile | null = null;
const processedProfile: JSSelfProfile | null = null;

/**
* Idempotent handler for profile stop
*/
function onProfileHandler(): void {
async function onProfileHandler(): Promise<null> {
// Check if the profile exists and return it the behavior has to be idempotent as users may call transaction.finish multiple times.
if (!transaction) {
return;
return null;
}
// Satisfy the type checker, but profiler will always be defined here.
if (!profiler) {
return;
return null;
}
if (processedProfile) {
if (__DEBUG_BUILD__) {
Expand All@@ -169,12 +169,12 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
'already exists, returning early',
);
}
return;
return null;
}

profiler
return profiler
.stop()
.then((p: JSSelfProfile): void => {
.then((p: JSSelfProfile): null => {
if (maxDurationTimeoutID) {
WINDOW.clearTimeout(maxDurationTimeoutID);
maxDurationTimeoutID = undefined;
Expand All@@ -192,16 +192,11 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
'this may indicate an overlapping transaction or a call to stopProfiling with a profile title that was never started',
);
}
return;
}

// If a profile has less than 2 samples, it is not useful and should be discarded.
if (p.samples.length < 2) {
return;
return null;
}

processedProfile = { ...p, profile_id: profileId };
sendProfile(profileId, processedProfile);
addProfileToMap(profileId, p);
return null;
})
.catch(error => {
if (__DEBUG_BUILD__) {
Expand All@@ -219,6 +214,7 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
transaction.name || transaction.description,
);
}
// If the timeout exceeds, we want to stop profiling, but not finish the transaction
void onProfileHandler();
}, MAX_PROFILE_DURATION_MS);

Expand All@@ -230,73 +226,26 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
* startProfiling is called after the call to startTransaction in order to avoid our own code from
* being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction.
*/
function profilingWrappedTransactionFinish(): Promise<Transaction> {
function profilingWrappedTransactionFinish(): Transaction {
if (!transaction) {
return originalFinish();
}
// onProfileHandler should always return the same profile even if this is called multiple times.
// Always call onProfileHandler to ensure stopProfiling is called and the timeout is cleared.
onProfileHandler();

// Set profile context
transaction.setContext('profile', { profile_id: profileId });
void onProfileHandler().then(
() => {
transaction.setContext('profile', { profile_id: profileId });
originalFinish();
},
() => {
// If onProfileHandler fails, we still want to call the original finish method.
originalFinish();
},
);

return originalFinish();
return transaction;
}

transaction.finish = profilingWrappedTransactionFinish;
return transaction;
}

/**
* Wraps startTransaction with profiling logic. This is done automatically by the profiling integration.
*/
function __PRIVATE__wrapStartTransactionWithProfiling(startTransaction: StartTransaction): StartTransaction {
return function wrappedStartTransaction(
this: Hub,
transactionContext: TransactionContext,
customSamplingContext?: CustomSamplingContext,
): Transaction | undefined {
const transaction: Transaction | undefined = startTransaction.call(this, transactionContext, customSamplingContext);
if (transaction === undefined) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Transaction is undefined, skipping profiling');
}
return transaction;
}

return wrapTransactionWithProfiling(transaction);
};
}

/**
* Patches startTransaction and stopTransaction with profiling logic.
*/
export function addProfilingExtensionMethods(): void {
const carrier = getMainCarrier();
if (!carrier.__SENTRY__) {
if (__DEBUG_BUILD__) {
logger.log("[Profiling] Can't find main carrier, profiling won't work.");
}
return;
}
carrier.__SENTRY__.extensions = carrier.__SENTRY__.extensions || {};

if (!carrier.__SENTRY__.extensions['startTransaction']) {
if (__DEBUG_BUILD__) {
logger.log(
'[Profiling] startTransaction does not exists, profiling will not work. Make sure you import @sentry/tracing package before @sentry/profiling-node as import order matters.',
);
}
return;
}

if (__DEBUG_BUILD__) {
logger.log('[Profiling] startTransaction exists, patching it with profiling functionality...');
}

carrier.__SENTRY__.extensions['startTransaction'] = __PRIVATE__wrapStartTransactionWithProfiling(
// This is already patched by sentry/tracing, we are going to re-patch it...
carrier.__SENTRY__.extensions['startTransaction'] as StartTransaction,
);
}
92 changes: 66 additions & 26 deletions packages/browser/src/profiling/integration.ts
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
import type { Event, EventProcessor, Integration } from '@sentry/types';
import type { EventProcessor, Hub, Integration, Transaction } from '@sentry/types';
import type { Profile } from '@sentry/types/src/profiling';
import { logger } from '@sentry/utils';

import { PROFILING_EVENT_CACHE } from './cache';
import { addProfilingExtensionMethods } from './hubextensions';
import type { BrowserClient } from './../client';
import { wrapTransactionWithProfiling } from './hubextensions';
import type { ProfiledEvent } from './utils';
import {
addProfilesToEnvelope,
createProfilingEvent,
findProfiledTransactionsFromEnvelope,
PROFILE_MAP,
} from './utils';

/**
* Browser profiling integration. Stores any event that has contexts["profile"]["profile_id"]
Expand All@@ -15,34 +23,66 @@ import { addProfilingExtensionMethods } from './hubextensions';
*/
export class BrowserProfilingIntegration implements Integration {
public readonly name: string = 'BrowserProfilingIntegration';
public getCurrentHub?: () => Hub = undefined;

/**
* @inheritDoc
*/
public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void): void {
// Patching the hub to add the extension methods.
// Warning: we have an implicit dependency on import order and we will fail patching if the constructor of
// BrowserProfilingIntegration is called before @sentry/tracing is imported. This is because we need to patch
// the methods of @sentry/tracing which are patched as a side effect of importing @sentry/tracing.
addProfilingExtensionMethods();

// Add our event processor
addGlobalEventProcessor(this.handleGlobalEvent.bind(this));
}
public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void {
this.getCurrentHub = getCurrentHub;
const client = this.getCurrentHub().getClient() as BrowserClient;

/**
* @inheritDoc
*/
public handleGlobalEvent(event: Event): Event {
const profileId = event.contexts && event.contexts['profile'] && event.contexts['profile']['profile_id'];

if (profileId && typeof profileId === 'string') {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Profiling event found, caching it.');
}
PROFILING_EVENT_CACHE.add(profileId, event);
}
if (client && typeof client.on === 'function') {
client.on('startTransaction', (transaction: Transaction) => {
wrapTransactionWithProfiling(transaction);
});

client.on('beforeEnvelope', (envelope): void => {
// if not profiles are in queue, there is nothing to add to the envelope.
if (!PROFILE_MAP['size']) {
return;
}

const profiledTransactionEvents = findProfiledTransactionsFromEnvelope(envelope);
if (!profiledTransactionEvents.length) {
return;
}

const profilesToAddToEnvelope: Profile[] = [];

return event;
for (const profiledTransaction of profiledTransactionEvents) {
const context = profiledTransaction && profiledTransaction.contexts;
const profile_id = context && context['profile'] && (context['profile']['profile_id'] as string);

if (!profile_id) {
__DEBUG_BUILD__ &&
logger.log('[Profiling] cannot find profile for a transaction without a profile context');
continue;
}

// Remove the profile from the transaction context before sending, relay will take care of the rest.
if (context && context['profile']) {
delete context.profile;
}

const profile = PROFILE_MAP.get(profile_id);
if (!profile) {
__DEBUG_BUILD__ && logger.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`);
continue;
}

PROFILE_MAP.delete(profile_id);
const profileEvent = createProfilingEvent(profile_id, profile, profiledTransaction as ProfiledEvent);

if (profileEvent) {
profilesToAddToEnvelope.push(profileEvent);
}
}

addProfilesToEnvelope(envelope, profilesToAddToEnvelope);
});
} else {
logger.warn('[Profiling] Client does not support hooks, profiling will be disabled');
}
}
}
8 changes: 0 additions & 8 deletions packages/browser/src/profiling/jsSelfProfiling.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,10 +26,6 @@ export type JSSelfProfile = {
samples: JSSelfProfileSample[];
};

export interface ProcessedJSSelfProfile extends JSSelfProfile {
profile_id: string;
}

type BufferFullCallback = (trace: JSSelfProfile) => void;

export interface JSSelfProfiler {
Expand All@@ -49,7 +45,3 @@ declare global {
Profiler: typeof JSSelfProfilerConstructor | undefined;
}
}

export interface RawThreadCpuProfile extends JSSelfProfile {
profile_id: string;
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
171 changes: 60 additions & 111 deletions packages/browser/src/profiling/hubextensions.ts
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,17 @@
import { getCurrentHub, getMainCarrier } from '@sentry/core';
import type { CustomSamplingContext, Hub, Transaction, TransactionContext } from '@sentry/types';
/* eslint-disable complexity */
import { getCurrentHub } from '@sentry/core';
import type { Transaction } from '@sentry/types';
import { logger, uuid4 } from '@sentry/utils';

import { WINDOW } from '../helpers';
import type {
JSSelfProfile,
JSSelfProfiler,
JSSelfProfilerConstructor,
ProcessedJSSelfProfile,
} from './jsSelfProfiling';
import { sendProfile } from './sendProfile';
import type { JSSelfProfile, JSSelfProfiler, JSSelfProfilerConstructor } from './jsSelfProfiling';
import { addProfileToMap, isValidSampleRate } from './utils';

// Max profile duration.
const MAX_PROFILE_DURATION_MS = 30_000;
export const MAX_PROFILE_DURATION_MS = 30_000;
// Keep a flag value to avoid re-initializing the profiler constructor. If it fails
// once, it will always fail and this allows us to early return.
let PROFILING_CONSTRUCTOR_FAILED = false;

// While we experiment, per transaction sampling interval will be more flexible to work with.
type StartTransaction = (
this: Hub,
transactionContext: TransactionContext,
customSamplingContext?: CustomSamplingContext,
) => Transaction | undefined;

/**
* Check if profiler constructor is available.
* @param maybeProfiler
Expand DownExpand Up@@ -55,7 +43,7 @@ export function onProfilingStartRouteTransaction(transaction: Transaction | unde
* startProfiling is called after the call to startTransaction in order to avoid our own code from
* being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction.
*/
function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
export function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
// Feature support check first
const JSProfilerConstructor = WINDOW.Profiler;

Expand All@@ -68,14 +56,6 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
return transaction;
}

// profilesSampleRate is multiplied with tracesSampleRate to get the final sampling rate.
if (!transaction.sampled) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Transaction is not sampled, skipping profiling');
}
return transaction;
}

// If constructor failed once, it will always fail, so we can early return.
if (PROFILING_CONSTRUCTOR_FAILED) {
if (__DEBUG_BUILD__) {
Expand All@@ -86,21 +66,41 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {

const client = getCurrentHub().getClient();
const options = client && client.getOptions();
if (!options) {
__DEBUG_BUILD__ && logger.log('[Profiling] Profiling disabled, no options found.');
return transaction;
}

// @ts-ignore not part of the browser options yet
const profilesSampleRate = (options && options.profilesSampleRate) || 0;
if (profilesSampleRate === undefined) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Profiling disabled, enable it by setting `profilesSampleRate` option to SDK init call.');
}
// @ts-ignore profilesSampleRate is not part of the browser options yet
const profilesSampleRate: number | boolean | undefined = options.profilesSampleRate;

// Since this is coming from the user (or from a function provided by the user), who knows what we might get. (The
// only valid values are booleans or numbers between 0 and 1.)
if (!isValidSampleRate(profilesSampleRate)) {
__DEBUG_BUILD__ && logger.warn('[Profiling] Discarding profile because of invalid sample rate.');
return transaction;
}

// if the function returned 0 (or false), or if `profileSampleRate` is 0, it's a sign the profile should be dropped
if (!profilesSampleRate) {
__DEBUG_BUILD__ &&
logger.log(
'[Profiling] Discarding profile because a negative sampling decision was inherited or profileSampleRate is set to 0',
);
return transaction;
}

// Now we roll the dice. Math.random is inclusive of 0, but not of 1, so strict < is safe here. In case sampleRate is
// a boolean, the < comparison will cause it to be automatically cast to 1 if it's true and 0 if it's false.
const sampled = profilesSampleRate === true ? true : Math.random() < profilesSampleRate;
// Check if we should sample this profile
if (Math.random() > profilesSampleRate) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Skip profiling transaction due to sampling.');
}
if (!sampled) {
__DEBUG_BUILD__ &&
logger.log(
`[Profiling] Discarding profile because it's not included in the random sample (sampling rate = ${Number(
profilesSampleRate,
)})`,
);
return transaction;
}

Expand DownExpand Up@@ -147,19 +147,19 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
// event of an error or user mistake (calling transaction.finish multiple times), it is important that the behavior of onProfileHandler
// is idempotent as we do not want any timings or profiles to be overriden by the last call to onProfileHandler.
// After the original finish method is called, the event will be reported through the integration and delegated to transport.
let processedProfile: ProcessedJSSelfProfile | null = null;
const processedProfile: JSSelfProfile | null = null;

/**
* Idempotent handler for profile stop
*/
function onProfileHandler(): void {
async function onProfileHandler(): Promise<null> {
// Check if the profile exists and return it the behavior has to be idempotent as users may call transaction.finish multiple times.
if (!transaction) {
return;
return null;
}
// Satisfy the type checker, but profiler will always be defined here.
if (!profiler) {
return;
return null;
}
if (processedProfile) {
if (__DEBUG_BUILD__) {
Expand All@@ -169,12 +169,12 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
'already exists, returning early',
);
}
return;
return null;
}

profiler
return profiler
.stop()
.then((p: JSSelfProfile): void => {
.then((p: JSSelfProfile): null => {
if (maxDurationTimeoutID) {
WINDOW.clearTimeout(maxDurationTimeoutID);
maxDurationTimeoutID = undefined;
Expand All@@ -192,16 +192,11 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
'this may indicate an overlapping transaction or a call to stopProfiling with a profile title that was never started',
);
}
return;
}

// If a profile has less than 2 samples, it is not useful and should be discarded.
if (p.samples.length < 2) {
return;
return null;
}

processedProfile = { ...p, profile_id: profileId };
sendProfile(profileId, processedProfile);
addProfileToMap(profileId, p);
return null;
})
.catch(error => {
if (__DEBUG_BUILD__) {
Expand All@@ -219,6 +214,7 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
transaction.name || transaction.description,
);
}
// If the timeout exceeds, we want to stop profiling, but not finish the transaction
void onProfileHandler();
}, MAX_PROFILE_DURATION_MS);

Expand All@@ -230,73 +226,26 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
* startProfiling is called after the call to startTransaction in order to avoid our own code from
* being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction.
*/
function profilingWrappedTransactionFinish(): Promise<Transaction> {
function profilingWrappedTransactionFinish(): Transaction {
if (!transaction) {
return originalFinish();
}
// onProfileHandler should always return the same profile even if this is called multiple times.
// Always call onProfileHandler to ensure stopProfiling is called and the timeout is cleared.
onProfileHandler();

// Set profile context
transaction.setContext('profile', { profile_id: profileId });
void onProfileHandler().then(
() => {
transaction.setContext('profile', { profile_id: profileId });
originalFinish();
},
() => {
// If onProfileHandler fails, we still want to call the original finish method.
originalFinish();
},
);

return originalFinish();
return transaction;
}

transaction.finish = profilingWrappedTransactionFinish;
return transaction;
}

/**
* Wraps startTransaction with profiling logic. This is done automatically by the profiling integration.
*/
function __PRIVATE__wrapStartTransactionWithProfiling(startTransaction: StartTransaction): StartTransaction {
return function wrappedStartTransaction(
this: Hub,
transactionContext: TransactionContext,
customSamplingContext?: CustomSamplingContext,
): Transaction | undefined {
const transaction: Transaction | undefined = startTransaction.call(this, transactionContext, customSamplingContext);
if (transaction === undefined) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Transaction is undefined, skipping profiling');
}
return transaction;
}

return wrapTransactionWithProfiling(transaction);
};
}

/**
* Patches startTransaction and stopTransaction with profiling logic.
*/
export function addProfilingExtensionMethods(): void {
const carrier = getMainCarrier();
if (!carrier.__SENTRY__) {
if (__DEBUG_BUILD__) {
logger.log("[Profiling] Can't find main carrier, profiling won't work.");
}
return;
}
carrier.__SENTRY__.extensions = carrier.__SENTRY__.extensions || {};

if (!carrier.__SENTRY__.extensions['startTransaction']) {
if (__DEBUG_BUILD__) {
logger.log(
'[Profiling] startTransaction does not exists, profiling will not work. Make sure you import @sentry/tracing package before @sentry/profiling-node as import order matters.',
);
}
return;
}

if (__DEBUG_BUILD__) {
logger.log('[Profiling] startTransaction exists, patching it with profiling functionality...');
}

carrier.__SENTRY__.extensions['startTransaction'] = __PRIVATE__wrapStartTransactionWithProfiling(
// This is already patched by sentry/tracing, we are going to re-patch it...
carrier.__SENTRY__.extensions['startTransaction'] as StartTransaction,
);
}
92 changes: 66 additions & 26 deletions packages/browser/src/profiling/integration.ts
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
import type { Event, EventProcessor, Integration } from '@sentry/types';
import type { EventProcessor, Hub, Integration, Transaction } from '@sentry/types';
import type { Profile } from '@sentry/types/src/profiling';
import { logger } from '@sentry/utils';

import { PROFILING_EVENT_CACHE } from './cache';
import { addProfilingExtensionMethods } from './hubextensions';
import type { BrowserClient } from './../client';
import { wrapTransactionWithProfiling } from './hubextensions';
import type { ProfiledEvent } from './utils';
import {
addProfilesToEnvelope,
createProfilingEvent,
findProfiledTransactionsFromEnvelope,
PROFILE_MAP,
} from './utils';

/**
* Browser profiling integration. Stores any event that has contexts["profile"]["profile_id"]
Expand All@@ -15,34 +23,66 @@ import { addProfilingExtensionMethods } from './hubextensions';
*/
export class BrowserProfilingIntegration implements Integration {
public readonly name: string = 'BrowserProfilingIntegration';
public getCurrentHub?: () => Hub = undefined;

/**
* @inheritDoc
*/
public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void): void {
// Patching the hub to add the extension methods.
// Warning: we have an implicit dependency on import order and we will fail patching if the constructor of
// BrowserProfilingIntegration is called before @sentry/tracing is imported. This is because we need to patch
// the methods of @sentry/tracing which are patched as a side effect of importing @sentry/tracing.
addProfilingExtensionMethods();

// Add our event processor
addGlobalEventProcessor(this.handleGlobalEvent.bind(this));
}
public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void {
this.getCurrentHub = getCurrentHub;
const client = this.getCurrentHub().getClient() as BrowserClient;

/**
* @inheritDoc
*/
public handleGlobalEvent(event: Event): Event {
const profileId = event.contexts && event.contexts['profile'] && event.contexts['profile']['profile_id'];

if (profileId && typeof profileId === 'string') {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Profiling event found, caching it.');
}
PROFILING_EVENT_CACHE.add(profileId, event);
}
if (client && typeof client.on === 'function') {
client.on('startTransaction', (transaction: Transaction) => {
wrapTransactionWithProfiling(transaction);
});

client.on('beforeEnvelope', (envelope): void => {
// if not profiles are in queue, there is nothing to add to the envelope.
if (!PROFILE_MAP['size']) {
return;
}

const profiledTransactionEvents = findProfiledTransactionsFromEnvelope(envelope);
if (!profiledTransactionEvents.length) {
return;
}

const profilesToAddToEnvelope: Profile[] = [];

return event;
for (const profiledTransaction of profiledTransactionEvents) {
const context = profiledTransaction && profiledTransaction.contexts;
const profile_id = context && context['profile'] && (context['profile']['profile_id'] as string);

if (!profile_id) {
__DEBUG_BUILD__ &&
logger.log('[Profiling] cannot find profile for a transaction without a profile context');
continue;
}

// Remove the profile from the transaction context before sending, relay will take care of the rest.
if (context && context['profile']) {
delete context.profile;
}

const profile = PROFILE_MAP.get(profile_id);
if (!profile) {
__DEBUG_BUILD__ && logger.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`);
continue;
}

PROFILE_MAP.delete(profile_id);
const profileEvent = createProfilingEvent(profile_id, profile, profiledTransaction as ProfiledEvent);

if (profileEvent) {
profilesToAddToEnvelope.push(profileEvent);
}
}

addProfilesToEnvelope(envelope, profilesToAddToEnvelope);
});
} else {
logger.warn('[Profiling] Client does not support hooks, profiling will be disabled');
}
}
}
8 changes: 0 additions & 8 deletions packages/browser/src/profiling/jsSelfProfiling.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,10 +26,6 @@ export type JSSelfProfile = {
samples: JSSelfProfileSample[];
};

export interface ProcessedJSSelfProfile extends JSSelfProfile {
profile_id: string;
}

type BufferFullCallback = (trace: JSSelfProfile) => void;

export interface JSSelfProfiler {
Expand All@@ -49,7 +45,3 @@ declare global {
Profiler: typeof JSSelfProfilerConstructor | undefined;
}
}

export interface RawThreadCpuProfile extends JSSelfProfile {
profile_id: string;
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
171 changes: 60 additions & 111 deletions packages/browser/src/profiling/hubextensions.ts
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,17 @@
import { getCurrentHub, getMainCarrier } from '@sentry/core';
import type { CustomSamplingContext, Hub, Transaction, TransactionContext } from '@sentry/types';
/* eslint-disable complexity */
import { getCurrentHub } from '@sentry/core';
import type { Transaction } from '@sentry/types';
import { logger, uuid4 } from '@sentry/utils';

import { WINDOW } from '../helpers';
import type {
JSSelfProfile,
JSSelfProfiler,
JSSelfProfilerConstructor,
ProcessedJSSelfProfile,
} from './jsSelfProfiling';
import { sendProfile } from './sendProfile';
import type { JSSelfProfile, JSSelfProfiler, JSSelfProfilerConstructor } from './jsSelfProfiling';
import { addProfileToMap, isValidSampleRate } from './utils';

// Max profile duration.
const MAX_PROFILE_DURATION_MS = 30_000;
export const MAX_PROFILE_DURATION_MS = 30_000;
// Keep a flag value to avoid re-initializing the profiler constructor. If it fails
// once, it will always fail and this allows us to early return.
let PROFILING_CONSTRUCTOR_FAILED = false;

// While we experiment, per transaction sampling interval will be more flexible to work with.
type StartTransaction = (
this: Hub,
transactionContext: TransactionContext,
customSamplingContext?: CustomSamplingContext,
) => Transaction | undefined;

/**
* Check if profiler constructor is available.
* @param maybeProfiler
Expand DownExpand Up@@ -55,7 +43,7 @@ export function onProfilingStartRouteTransaction(transaction: Transaction | unde
* startProfiling is called after the call to startTransaction in order to avoid our own code from
* being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction.
*/
function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
export function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
// Feature support check first
const JSProfilerConstructor = WINDOW.Profiler;

Expand All@@ -68,14 +56,6 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
return transaction;
}

// profilesSampleRate is multiplied with tracesSampleRate to get the final sampling rate.
if (!transaction.sampled) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Transaction is not sampled, skipping profiling');
}
return transaction;
}

// If constructor failed once, it will always fail, so we can early return.
if (PROFILING_CONSTRUCTOR_FAILED) {
if (__DEBUG_BUILD__) {
Expand All@@ -86,21 +66,41 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {

const client = getCurrentHub().getClient();
const options = client && client.getOptions();
if (!options) {
__DEBUG_BUILD__ && logger.log('[Profiling] Profiling disabled, no options found.');
return transaction;
}

// @ts-ignore not part of the browser options yet
const profilesSampleRate = (options && options.profilesSampleRate) || 0;
if (profilesSampleRate === undefined) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Profiling disabled, enable it by setting `profilesSampleRate` option to SDK init call.');
}
// @ts-ignore profilesSampleRate is not part of the browser options yet
const profilesSampleRate: number | boolean | undefined = options.profilesSampleRate;

// Since this is coming from the user (or from a function provided by the user), who knows what we might get. (The
// only valid values are booleans or numbers between 0 and 1.)
if (!isValidSampleRate(profilesSampleRate)) {
__DEBUG_BUILD__ && logger.warn('[Profiling] Discarding profile because of invalid sample rate.');
return transaction;
}

// if the function returned 0 (or false), or if `profileSampleRate` is 0, it's a sign the profile should be dropped
if (!profilesSampleRate) {
__DEBUG_BUILD__ &&
logger.log(
'[Profiling] Discarding profile because a negative sampling decision was inherited or profileSampleRate is set to 0',
);
return transaction;
}

// Now we roll the dice. Math.random is inclusive of 0, but not of 1, so strict < is safe here. In case sampleRate is
// a boolean, the < comparison will cause it to be automatically cast to 1 if it's true and 0 if it's false.
const sampled = profilesSampleRate === true ? true : Math.random() < profilesSampleRate;
// Check if we should sample this profile
if (Math.random() > profilesSampleRate) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Skip profiling transaction due to sampling.');
}
if (!sampled) {
__DEBUG_BUILD__ &&
logger.log(
`[Profiling] Discarding profile because it's not included in the random sample (sampling rate = ${Number(
profilesSampleRate,
)})`,
);
return transaction;
}

Expand DownExpand Up@@ -147,19 +147,19 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
// event of an error or user mistake (calling transaction.finish multiple times), it is important that the behavior of onProfileHandler
// is idempotent as we do not want any timings or profiles to be overriden by the last call to onProfileHandler.
// After the original finish method is called, the event will be reported through the integration and delegated to transport.
let processedProfile: ProcessedJSSelfProfile | null = null;
const processedProfile: JSSelfProfile | null = null;

/**
* Idempotent handler for profile stop
*/
function onProfileHandler(): void {
async function onProfileHandler(): Promise<null> {
// Check if the profile exists and return it the behavior has to be idempotent as users may call transaction.finish multiple times.
if (!transaction) {
return;
return null;
}
// Satisfy the type checker, but profiler will always be defined here.
if (!profiler) {
return;
return null;
}
if (processedProfile) {
if (__DEBUG_BUILD__) {
Expand All@@ -169,12 +169,12 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
'already exists, returning early',
);
}
return;
return null;
}

profiler
return profiler
.stop()
.then((p: JSSelfProfile): void => {
.then((p: JSSelfProfile): null => {
if (maxDurationTimeoutID) {
WINDOW.clearTimeout(maxDurationTimeoutID);
maxDurationTimeoutID = undefined;
Expand All@@ -192,16 +192,11 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
'this may indicate an overlapping transaction or a call to stopProfiling with a profile title that was never started',
);
}
return;
}

// If a profile has less than 2 samples, it is not useful and should be discarded.
if (p.samples.length < 2) {
return;
return null;
}

processedProfile = { ...p, profile_id: profileId };
sendProfile(profileId, processedProfile);
addProfileToMap(profileId, p);
return null;
})
.catch(error => {
if (__DEBUG_BUILD__) {
Expand All@@ -219,6 +214,7 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
transaction.name || transaction.description,
);
}
// If the timeout exceeds, we want to stop profiling, but not finish the transaction
void onProfileHandler();
}, MAX_PROFILE_DURATION_MS);

Expand All@@ -230,73 +226,26 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
* startProfiling is called after the call to startTransaction in order to avoid our own code from
* being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction.
*/
function profilingWrappedTransactionFinish(): Promise<Transaction> {
function profilingWrappedTransactionFinish(): Transaction {
if (!transaction) {
return originalFinish();
}
// onProfileHandler should always return the same profile even if this is called multiple times.
// Always call onProfileHandler to ensure stopProfiling is called and the timeout is cleared.
onProfileHandler();

// Set profile context
transaction.setContext('profile', { profile_id: profileId });
void onProfileHandler().then(
() => {
transaction.setContext('profile', { profile_id: profileId });
originalFinish();
},
() => {
// If onProfileHandler fails, we still want to call the original finish method.
originalFinish();
},
);

return originalFinish();
return transaction;
}

transaction.finish = profilingWrappedTransactionFinish;
return transaction;
}

/**
* Wraps startTransaction with profiling logic. This is done automatically by the profiling integration.
*/
function __PRIVATE__wrapStartTransactionWithProfiling(startTransaction: StartTransaction): StartTransaction {
return function wrappedStartTransaction(
this: Hub,
transactionContext: TransactionContext,
customSamplingContext?: CustomSamplingContext,
): Transaction | undefined {
const transaction: Transaction | undefined = startTransaction.call(this, transactionContext, customSamplingContext);
if (transaction === undefined) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Transaction is undefined, skipping profiling');
}
return transaction;
}

return wrapTransactionWithProfiling(transaction);
};
}

/**
* Patches startTransaction and stopTransaction with profiling logic.
*/
export function addProfilingExtensionMethods(): void {
const carrier = getMainCarrier();
if (!carrier.__SENTRY__) {
if (__DEBUG_BUILD__) {
logger.log("[Profiling] Can't find main carrier, profiling won't work.");
}
return;
}
carrier.__SENTRY__.extensions = carrier.__SENTRY__.extensions || {};

if (!carrier.__SENTRY__.extensions['startTransaction']) {
if (__DEBUG_BUILD__) {
logger.log(
'[Profiling] startTransaction does not exists, profiling will not work. Make sure you import @sentry/tracing package before @sentry/profiling-node as import order matters.',
);
}
return;
}

if (__DEBUG_BUILD__) {
logger.log('[Profiling] startTransaction exists, patching it with profiling functionality...');
}

carrier.__SENTRY__.extensions['startTransaction'] = __PRIVATE__wrapStartTransactionWithProfiling(
// This is already patched by sentry/tracing, we are going to re-patch it...
carrier.__SENTRY__.extensions['startTransaction'] as StartTransaction,
);
}
92 changes: 66 additions & 26 deletions packages/browser/src/profiling/integration.ts
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
import type { Event, EventProcessor, Integration } from '@sentry/types';
import type { EventProcessor, Hub, Integration, Transaction } from '@sentry/types';
import type { Profile } from '@sentry/types/src/profiling';
import { logger } from '@sentry/utils';

import { PROFILING_EVENT_CACHE } from './cache';
import { addProfilingExtensionMethods } from './hubextensions';
import type { BrowserClient } from './../client';
import { wrapTransactionWithProfiling } from './hubextensions';
import type { ProfiledEvent } from './utils';
import {
addProfilesToEnvelope,
createProfilingEvent,
findProfiledTransactionsFromEnvelope,
PROFILE_MAP,
} from './utils';

/**
* Browser profiling integration. Stores any event that has contexts["profile"]["profile_id"]
Expand All@@ -15,34 +23,66 @@ import { addProfilingExtensionMethods } from './hubextensions';
*/
export class BrowserProfilingIntegration implements Integration {
public readonly name: string = 'BrowserProfilingIntegration';
public getCurrentHub?: () => Hub = undefined;

/**
* @inheritDoc
*/
public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void): void {
// Patching the hub to add the extension methods.
// Warning: we have an implicit dependency on import order and we will fail patching if the constructor of
// BrowserProfilingIntegration is called before @sentry/tracing is imported. This is because we need to patch
// the methods of @sentry/tracing which are patched as a side effect of importing @sentry/tracing.
addProfilingExtensionMethods();

// Add our event processor
addGlobalEventProcessor(this.handleGlobalEvent.bind(this));
}
public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void {
this.getCurrentHub = getCurrentHub;
const client = this.getCurrentHub().getClient() as BrowserClient;

/**
* @inheritDoc
*/
public handleGlobalEvent(event: Event): Event {
const profileId = event.contexts && event.contexts['profile'] && event.contexts['profile']['profile_id'];

if (profileId && typeof profileId === 'string') {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Profiling event found, caching it.');
}
PROFILING_EVENT_CACHE.add(profileId, event);
}
if (client && typeof client.on === 'function') {
client.on('startTransaction', (transaction: Transaction) => {
wrapTransactionWithProfiling(transaction);
});

client.on('beforeEnvelope', (envelope): void => {
// if not profiles are in queue, there is nothing to add to the envelope.
if (!PROFILE_MAP['size']) {
return;
}

const profiledTransactionEvents = findProfiledTransactionsFromEnvelope(envelope);
if (!profiledTransactionEvents.length) {
return;
}

const profilesToAddToEnvelope: Profile[] = [];

return event;
for (const profiledTransaction of profiledTransactionEvents) {
const context = profiledTransaction && profiledTransaction.contexts;
const profile_id = context && context['profile'] && (context['profile']['profile_id'] as string);

if (!profile_id) {
__DEBUG_BUILD__ &&
logger.log('[Profiling] cannot find profile for a transaction without a profile context');
continue;
}

// Remove the profile from the transaction context before sending, relay will take care of the rest.
if (context && context['profile']) {
delete context.profile;
}

const profile = PROFILE_MAP.get(profile_id);
if (!profile) {
__DEBUG_BUILD__ && logger.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`);
continue;
}

PROFILE_MAP.delete(profile_id);
const profileEvent = createProfilingEvent(profile_id, profile, profiledTransaction as ProfiledEvent);

if (profileEvent) {
profilesToAddToEnvelope.push(profileEvent);
}
}

addProfilesToEnvelope(envelope, profilesToAddToEnvelope);
});
} else {
logger.warn('[Profiling] Client does not support hooks, profiling will be disabled');
}
}
}
8 changes: 0 additions & 8 deletions packages/browser/src/profiling/jsSelfProfiling.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,10 +26,6 @@ export type JSSelfProfile = {
samples: JSSelfProfileSample[];
};

export interface ProcessedJSSelfProfile extends JSSelfProfile {
profile_id: string;
}

type BufferFullCallback = (trace: JSSelfProfile) => void;

export interface JSSelfProfiler {
Expand All@@ -49,7 +45,3 @@ declare global {
Profiler: typeof JSSelfProfilerConstructor | undefined;
}
}

export interface RawThreadCpuProfile extends JSSelfProfile {
profile_id: string;
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
171 changes: 60 additions & 111 deletions packages/browser/src/profiling/hubextensions.ts
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,17 @@
import { getCurrentHub, getMainCarrier } from '@sentry/core';
import type { CustomSamplingContext, Hub, Transaction, TransactionContext } from '@sentry/types';
/* eslint-disable complexity */
import { getCurrentHub } from '@sentry/core';
import type { Transaction } from '@sentry/types';
import { logger, uuid4 } from '@sentry/utils';

import { WINDOW } from '../helpers';
import type {
JSSelfProfile,
JSSelfProfiler,
JSSelfProfilerConstructor,
ProcessedJSSelfProfile,
} from './jsSelfProfiling';
import { sendProfile } from './sendProfile';
import type { JSSelfProfile, JSSelfProfiler, JSSelfProfilerConstructor } from './jsSelfProfiling';
import { addProfileToMap, isValidSampleRate } from './utils';

// Max profile duration.
const MAX_PROFILE_DURATION_MS = 30_000;
export const MAX_PROFILE_DURATION_MS = 30_000;
// Keep a flag value to avoid re-initializing the profiler constructor. If it fails
// once, it will always fail and this allows us to early return.
let PROFILING_CONSTRUCTOR_FAILED = false;

// While we experiment, per transaction sampling interval will be more flexible to work with.
type StartTransaction = (
this: Hub,
transactionContext: TransactionContext,
customSamplingContext?: CustomSamplingContext,
) => Transaction | undefined;

/**
* Check if profiler constructor is available.
* @param maybeProfiler
Expand DownExpand Up@@ -55,7 +43,7 @@ export function onProfilingStartRouteTransaction(transaction: Transaction | unde
* startProfiling is called after the call to startTransaction in order to avoid our own code from
* being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction.
*/
function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
export function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
// Feature support check first
const JSProfilerConstructor = WINDOW.Profiler;

Expand All@@ -68,14 +56,6 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
return transaction;
}

// profilesSampleRate is multiplied with tracesSampleRate to get the final sampling rate.
if (!transaction.sampled) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Transaction is not sampled, skipping profiling');
}
return transaction;
}

// If constructor failed once, it will always fail, so we can early return.
if (PROFILING_CONSTRUCTOR_FAILED) {
if (__DEBUG_BUILD__) {
Expand All@@ -86,21 +66,41 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {

const client = getCurrentHub().getClient();
const options = client && client.getOptions();
if (!options) {
__DEBUG_BUILD__ && logger.log('[Profiling] Profiling disabled, no options found.');
return transaction;
}

// @ts-ignore not part of the browser options yet
const profilesSampleRate = (options && options.profilesSampleRate) || 0;
if (profilesSampleRate === undefined) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Profiling disabled, enable it by setting `profilesSampleRate` option to SDK init call.');
}
// @ts-ignore profilesSampleRate is not part of the browser options yet
const profilesSampleRate: number | boolean | undefined = options.profilesSampleRate;

// Since this is coming from the user (or from a function provided by the user), who knows what we might get. (The
// only valid values are booleans or numbers between 0 and 1.)
if (!isValidSampleRate(profilesSampleRate)) {
__DEBUG_BUILD__ && logger.warn('[Profiling] Discarding profile because of invalid sample rate.');
return transaction;
}

// if the function returned 0 (or false), or if `profileSampleRate` is 0, it's a sign the profile should be dropped
if (!profilesSampleRate) {
__DEBUG_BUILD__ &&
logger.log(
'[Profiling] Discarding profile because a negative sampling decision was inherited or profileSampleRate is set to 0',
);
return transaction;
}

// Now we roll the dice. Math.random is inclusive of 0, but not of 1, so strict < is safe here. In case sampleRate is
// a boolean, the < comparison will cause it to be automatically cast to 1 if it's true and 0 if it's false.
const sampled = profilesSampleRate === true ? true : Math.random() < profilesSampleRate;
// Check if we should sample this profile
if (Math.random() > profilesSampleRate) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Skip profiling transaction due to sampling.');
}
if (!sampled) {
__DEBUG_BUILD__ &&
logger.log(
`[Profiling] Discarding profile because it's not included in the random sample (sampling rate = ${Number(
profilesSampleRate,
)})`,
);
return transaction;
}

Expand DownExpand Up@@ -147,19 +147,19 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
// event of an error or user mistake (calling transaction.finish multiple times), it is important that the behavior of onProfileHandler
// is idempotent as we do not want any timings or profiles to be overriden by the last call to onProfileHandler.
// After the original finish method is called, the event will be reported through the integration and delegated to transport.
let processedProfile: ProcessedJSSelfProfile | null = null;
const processedProfile: JSSelfProfile | null = null;

/**
* Idempotent handler for profile stop
*/
function onProfileHandler(): void {
async function onProfileHandler(): Promise<null> {
// Check if the profile exists and return it the behavior has to be idempotent as users may call transaction.finish multiple times.
if (!transaction) {
return;
return null;
}
// Satisfy the type checker, but profiler will always be defined here.
if (!profiler) {
return;
return null;
}
if (processedProfile) {
if (__DEBUG_BUILD__) {
Expand All@@ -169,12 +169,12 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
'already exists, returning early',
);
}
return;
return null;
}

profiler
return profiler
.stop()
.then((p: JSSelfProfile): void => {
.then((p: JSSelfProfile): null => {
if (maxDurationTimeoutID) {
WINDOW.clearTimeout(maxDurationTimeoutID);
maxDurationTimeoutID = undefined;
Expand All@@ -192,16 +192,11 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
'this may indicate an overlapping transaction or a call to stopProfiling with a profile title that was never started',
);
}
return;
}

// If a profile has less than 2 samples, it is not useful and should be discarded.
if (p.samples.length < 2) {
return;
return null;
}

processedProfile = { ...p, profile_id: profileId };
sendProfile(profileId, processedProfile);
addProfileToMap(profileId, p);
return null;
})
.catch(error => {
if (__DEBUG_BUILD__) {
Expand All@@ -219,6 +214,7 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
transaction.name || transaction.description,
);
}
// If the timeout exceeds, we want to stop profiling, but not finish the transaction
void onProfileHandler();
}, MAX_PROFILE_DURATION_MS);

Expand All@@ -230,73 +226,26 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
* startProfiling is called after the call to startTransaction in order to avoid our own code from
* being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction.
*/
function profilingWrappedTransactionFinish(): Promise<Transaction> {
function profilingWrappedTransactionFinish(): Transaction {
if (!transaction) {
return originalFinish();
}
// onProfileHandler should always return the same profile even if this is called multiple times.
// Always call onProfileHandler to ensure stopProfiling is called and the timeout is cleared.
onProfileHandler();

// Set profile context
transaction.setContext('profile', { profile_id: profileId });
void onProfileHandler().then(
() => {
transaction.setContext('profile', { profile_id: profileId });
originalFinish();
},
() => {
// If onProfileHandler fails, we still want to call the original finish method.
originalFinish();
},
);

return originalFinish();
return transaction;
}

transaction.finish = profilingWrappedTransactionFinish;
return transaction;
}

/**
* Wraps startTransaction with profiling logic. This is done automatically by the profiling integration.
*/
function __PRIVATE__wrapStartTransactionWithProfiling(startTransaction: StartTransaction): StartTransaction {
return function wrappedStartTransaction(
this: Hub,
transactionContext: TransactionContext,
customSamplingContext?: CustomSamplingContext,
): Transaction | undefined {
const transaction: Transaction | undefined = startTransaction.call(this, transactionContext, customSamplingContext);
if (transaction === undefined) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Transaction is undefined, skipping profiling');
}
return transaction;
}

return wrapTransactionWithProfiling(transaction);
};
}

/**
* Patches startTransaction and stopTransaction with profiling logic.
*/
export function addProfilingExtensionMethods(): void {
const carrier = getMainCarrier();
if (!carrier.__SENTRY__) {
if (__DEBUG_BUILD__) {
logger.log("[Profiling] Can't find main carrier, profiling won't work.");
}
return;
}
carrier.__SENTRY__.extensions = carrier.__SENTRY__.extensions || {};

if (!carrier.__SENTRY__.extensions['startTransaction']) {
if (__DEBUG_BUILD__) {
logger.log(
'[Profiling] startTransaction does not exists, profiling will not work. Make sure you import @sentry/tracing package before @sentry/profiling-node as import order matters.',
);
}
return;
}

if (__DEBUG_BUILD__) {
logger.log('[Profiling] startTransaction exists, patching it with profiling functionality...');
}

carrier.__SENTRY__.extensions['startTransaction'] = __PRIVATE__wrapStartTransactionWithProfiling(
// This is already patched by sentry/tracing, we are going to re-patch it...
carrier.__SENTRY__.extensions['startTransaction'] as StartTransaction,
);
}
92 changes: 66 additions & 26 deletions packages/browser/src/profiling/integration.ts
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
import type { Event, EventProcessor, Integration } from '@sentry/types';
import type { EventProcessor, Hub, Integration, Transaction } from '@sentry/types';
import type { Profile } from '@sentry/types/src/profiling';
import { logger } from '@sentry/utils';

import { PROFILING_EVENT_CACHE } from './cache';
import { addProfilingExtensionMethods } from './hubextensions';
import type { BrowserClient } from './../client';
import { wrapTransactionWithProfiling } from './hubextensions';
import type { ProfiledEvent } from './utils';
import {
addProfilesToEnvelope,
createProfilingEvent,
findProfiledTransactionsFromEnvelope,
PROFILE_MAP,
} from './utils';

/**
* Browser profiling integration. Stores any event that has contexts["profile"]["profile_id"]
Expand All@@ -15,34 +23,66 @@ import { addProfilingExtensionMethods } from './hubextensions';
*/
export class BrowserProfilingIntegration implements Integration {
public readonly name: string = 'BrowserProfilingIntegration';
public getCurrentHub?: () => Hub = undefined;

/**
* @inheritDoc
*/
public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void): void {
// Patching the hub to add the extension methods.
// Warning: we have an implicit dependency on import order and we will fail patching if the constructor of
// BrowserProfilingIntegration is called before @sentry/tracing is imported. This is because we need to patch
// the methods of @sentry/tracing which are patched as a side effect of importing @sentry/tracing.
addProfilingExtensionMethods();

// Add our event processor
addGlobalEventProcessor(this.handleGlobalEvent.bind(this));
}
public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void {
this.getCurrentHub = getCurrentHub;
const client = this.getCurrentHub().getClient() as BrowserClient;

/**
* @inheritDoc
*/
public handleGlobalEvent(event: Event): Event {
const profileId = event.contexts && event.contexts['profile'] && event.contexts['profile']['profile_id'];

if (profileId && typeof profileId === 'string') {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Profiling event found, caching it.');
}
PROFILING_EVENT_CACHE.add(profileId, event);
}
if (client && typeof client.on === 'function') {
client.on('startTransaction', (transaction: Transaction) => {
wrapTransactionWithProfiling(transaction);
});

client.on('beforeEnvelope', (envelope): void => {
// if not profiles are in queue, there is nothing to add to the envelope.
if (!PROFILE_MAP['size']) {
return;
}

const profiledTransactionEvents = findProfiledTransactionsFromEnvelope(envelope);
if (!profiledTransactionEvents.length) {
return;
}

const profilesToAddToEnvelope: Profile[] = [];

return event;
for (const profiledTransaction of profiledTransactionEvents) {
const context = profiledTransaction && profiledTransaction.contexts;
const profile_id = context && context['profile'] && (context['profile']['profile_id'] as string);

if (!profile_id) {
__DEBUG_BUILD__ &&
logger.log('[Profiling] cannot find profile for a transaction without a profile context');
continue;
}

// Remove the profile from the transaction context before sending, relay will take care of the rest.
if (context && context['profile']) {
delete context.profile;
}

const profile = PROFILE_MAP.get(profile_id);
if (!profile) {
__DEBUG_BUILD__ && logger.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`);
continue;
}

PROFILE_MAP.delete(profile_id);
const profileEvent = createProfilingEvent(profile_id, profile, profiledTransaction as ProfiledEvent);

if (profileEvent) {
profilesToAddToEnvelope.push(profileEvent);
}
}

addProfilesToEnvelope(envelope, profilesToAddToEnvelope);
});
} else {
logger.warn('[Profiling] Client does not support hooks, profiling will be disabled');
}
}
}
8 changes: 0 additions & 8 deletions packages/browser/src/profiling/jsSelfProfiling.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,10 +26,6 @@ export type JSSelfProfile = {
samples: JSSelfProfileSample[];
};

export interface ProcessedJSSelfProfile extends JSSelfProfile {
profile_id: string;
}

type BufferFullCallback = (trace: JSSelfProfile) => void;

export interface JSSelfProfiler {
Expand All@@ -49,7 +45,3 @@ declare global {
Profiler: typeof JSSelfProfilerConstructor | undefined;
}
}

export interface RawThreadCpuProfile extends JSSelfProfile {
profile_id: string;
}
Loading