Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5ac4086
[PROF-13798] ✨ Add checkProfilingQuota admission API module
thomasbertet Apr 21, 2026
f4b0a0e
[PROF-13798] ✨ Gate profiling on quota admission API
thomasbertet Apr 21, 2026
7c5a3ec
[PROF-13798] 📝 Add profiling quota design spec and implementation plan
thomasbertet Apr 21, 2026
37c8a20
[PROF-13798] 🔥 Remove superpowers docs (moved to Confluence)
thomasbertet Apr 21, 2026
d4ea226
[PROF-13798] 🐛 Fix clientToken access in checkProfilingQuota
thomasbertet Apr 21, 2026
be037f8
[PROF-13798] 🐛 Fix TS error in profiler.spec.ts for quota-exceeded as…
thomasbertet Apr 22, 2026
b95e055
[PROF-13798] 👌 Fix lint errors (import order, curly braces, duplicate…
thomasbertet Apr 22, 2026
3d4539d
[PROF-13798] 🔊 Add telemetry debug logging to checkProfilingQuota
thomasbertet Apr 22, 2026
3465042
Revert "[PROF-13798] 🔊 Add telemetry debug logging to checkProfilingQ…
thomasbertet Apr 22, 2026
94fcc66
[PROF-13798] ✨ Use DD-CLIENT-TOKEN header and per-site base URL in ch…
thomasbertet Apr 23, 2026
fe025d5
[PROF-13798] ✨ Use quota.browser-intake-* URL pattern for checkProfil…
thomasbertet May 12, 2026
a1377a2
[PROF-13798] ✨ Redesign quota check API with QuotaResult, BackendQuot…
thomasbertet May 13, 2026
3e3f232
[PROF-13798] 🧹 Remove misleading comment in checkProfilingQuota
thomasbertet May 13, 2026
0c10968
[PROF-13798] 🐛 Fix TS error: pass clientToken to buildEndpointHost
thomasbertet May 13, 2026
db8591a
[PROF-13798] 👌 Fix lint: use typed helper to avoid unsafe-return in q…
thomasbertet May 13, 2026
525fb77
[PROF-13798] 🐛 Fix post-rebase breakages: buildEndpointHost signature…
thomasbertet May 13, 2026
a231bba
[PROF-13798] 🐛 Fix typecheck: remove minNumberOfSamples, add SessionR…
thomasbertet May 13, 2026
1177a58
[PROF-13798] 🐛 Fix E2E failures: route quota check through proxy to a…
thomasbertet May 13, 2026
e567433
Add optional host to work with intake subdomains
GianlucaBortoli May 14, 2026
5fa6403
[PROF-13798] 👌 Make host mandatory in ProxyFn
thomasbertet May 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/core/src/domain/configuration/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ type GenericBeforeSendCallback = (event: any, context?: any) => unknown
* path: /api/vX/product
* parameters: xxx=yyy&zzz=aaa
*/
export type ProxyFn = (options: { path: string; parameters: string }) => string
export type ProxyFn = (options: { path: string; parameters: string; host: string }) => string

/**
* @internal
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/domain/configuration/endpointBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,10 @@ function createEndpointUrlWithParametersBuilder(
const normalizedProxyUrl = normalizeUrl(proxy)
return (parameters) => `${normalizedProxyUrl}?ddforward=${encodeURIComponent(`${path}?${parameters}`)}`
}
const host = buildEndpointHost(initConfiguration)
if (typeof proxy === 'function') {
return (parameters) => proxy({ path, parameters })
return (parameters) => proxy({ path, parameters, host })
}
const host = buildEndpointHost(initConfiguration)
return (parameters) => `https://${host}${path}?${parameters}`
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Site } from '../intakeSites'
import { INTAKE_SITE_US1, INTAKE_URL_PARAMETERS } from '../intakeSites'
import type { InitConfiguration, SdkSource } from './configuration'
import type { InitConfiguration, ProxyFn, SdkSource } from './configuration'
import type { EndpointBuilder, TransportSource } from './endpointBuilder'
import { createEndpointBuilder } from './endpointBuilder'

Expand All @@ -14,6 +14,8 @@ export interface TransportConfiguration {
debuggerEndpointBuilder: EndpointBuilder
datacenter?: string | undefined
replica?: ReplicaConfiguration
clientToken: string
proxy?: string | ProxyFn | undefined
site: Site
source: SdkSource
}
Expand Down Expand Up @@ -55,6 +57,8 @@ export function computeTransportConfiguration(
const replicaConfiguration = computeReplicaConfiguration(resolvedConfiguration)

return {
clientToken: initConfiguration.clientToken,
proxy: initConfiguration.proxy,
replica: replicaConfiguration,
site,
source,
Expand Down
44 changes: 39 additions & 5 deletions packages/rum/src/domain/profiling/datadogProfiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import { assembleProfilingPayload } from './transport/assembly'
import { createLongTaskHistory } from './longTaskHistory'
import { createActionHistory } from './actionHistory'
import { createVitalHistory } from './vitalHistory'
import { checkProfilingQuota } from './quotaCheck'
import type { QuotaReason } from './quotaCheck'

export const DEFAULT_RUM_PROFILER_CONFIGURATION: RUMProfilerConfiguration = {
sampleIntervalMs: 10, // Sample stack trace every 10ms
Expand Down Expand Up @@ -58,6 +60,7 @@ export function createRumProfiler(
const vitalHistory = mockable(createVitalHistory)(lifeCycle)

let instance: RumProfilerInstance = { state: 'stopped', stateReason: 'initializing' }
let quotaCheckGeneration = 0

// Stops the profiler when session expires
lifeCycle.subscribe(LifeCycleEventType.SESSION_EXPIRED, () => {
Expand All @@ -66,7 +69,10 @@ export function createRumProfiler(

// Start the profiler again when session is renewed
lifeCycle.subscribe(LifeCycleEventType.SESSION_RENEWED, () => {
if (instance.state === 'stopped' && instance.stateReason === 'session-expired') {
if (
instance.state === 'stopped' &&
(instance.stateReason === 'session-expired' || instance.stateReason === 'quota_ko')
) {
start()
}
})
Expand Down Expand Up @@ -97,14 +103,35 @@ export function createRumProfiler(

// Start profiler instance
startNextProfilerInstance()

// Quota check — optimistic: profiler already recording, only gates sending.
// Generation counter invalidates results from a prior session (incremented on each start() call).
// State guard handles within-session cancellation (user stop, session expiry, etc.).
const checkGeneration = ++quotaCheckGeneration
const sessionId = session.findTrackedSession()?.id
if (sessionId) {
mockable(checkProfilingQuota)(configuration, sessionId)
.then((result) => {
if (checkGeneration !== quotaCheckGeneration) {
return
}
if (instance.state !== 'running' && instance.state !== 'paused') {
return
}
if (result.decision === 'quota_ko') {
stopProfiling('quota_ko', result.reason)
}
})
.catch(monitorError)
}
}

// Public API to manually stop the profiler.
function stop() {
stopProfiling('stopped-by-user')
}

function stopProfiling(reason: RumProfilerStoppedInstance['stateReason']) {
function stopProfiling(reason: RumProfilerStoppedInstance['stateReason'], quotaReason?: QuotaReason) {
// Stop current profiler instance (data collection happens async in background)
stopProfilerInstance(reason)

Expand All @@ -113,7 +140,7 @@ export function createRumProfiler(
globalCleanupTasks.length = 0

// Update Profiling status once the Profiler has been stopped.
profilingContextManager.set({ status: 'stopped', error_reason: undefined })
profilingContextManager.set({ status: 'stopped', error_reason: undefined, quota_reason: quotaReason })
}

/**
Expand Down Expand Up @@ -279,8 +306,15 @@ export function createRumProfiler(
// Cleanup instance-specific tasks (e.g., view listener)
runningInstance.cleanupTasks.forEach((cleanupTask) => cleanupTask())

// Collect and send profile data in background - doesn't block state transitions
collectProfilerInstance(runningInstance)
if (stateReason === 'quota_ko') {
// Discard data — quota denied means we should not send anything
clearTimeout(runningInstance.timeoutId)
runningInstance.profiler.removeEventListener('samplebufferfull', handleSampleBufferFull)
void runningInstance.profiler.stop().catch(monitorError)
} else {
// Collect and send profile data in background - doesn't block state transitions
collectProfilerInstance(runningInstance)
}
}

function pauseProfilerInstance() {
Expand Down
Loading
Loading