Repository: DataDog/browser-sdk
Pull request #3395: ✨ [RUM-7387] Add user id and account id to trace header
Author: login: cy-moi
Labels: name: staging-10
name: staging-11
Created: 2025-03-05T13:33:55Z | Merged: 2025-03-10T14:28:25Z

## Motivation
Send user id and account id in trace header so they could propagate.
[[RFC] - Adding User and Account Id
](https://docs.google.com/document/d/1W1rrr3tMscxTGmP-jaxrub6PSFyUGn8_fYssbL382q8/edit?tab=t.0)<!-- Why are you making this change, what problem does it solve? Include links to relevant tickets. -->

## Changes
Passed common context to tracer and add user id and account id in trace context.
Encode in base64 as the field was already in base64. ([Initial RFC on usr.id field](https://docs.google.com/document/d/1T3qAE5nol18psOaHESQ3r-WRiZWss9nyGmroShug8ao/edit?tab=t.0#heading=h.3wmduzc8mwe1))
<!-- What does this change exactly? Who will be affected? Include relevant screenshots, videos, links. -->

## Testing

<!-- How can the reviewer confirm these changes do what you say they do? Are there automated tests? -->

- [x] Local
- [ ] Staging
- [x] Unit
- [x] End to end

---

I have gone over the [contributing](https://github.com/DataDog/browser-sdk/blob/main/CONTRIBUTING.md) documentation.



Diff:
diff --git a/packages/core/src/tools/experimentalFeatures.ts b/packages/core/src/tools/experimentalFeatures.ts
index 439f1ce10b..3c3b7d37c8 100644
--- a/packages/core/src/tools/experimentalFeatures.ts
+++ b/packages/core/src/tools/experimentalFeatures.ts
@@ -16,6 +16,7 @@ import { objectHasValue } from './utils/objectUtils'
 export enum ExperimentalFeature {
   WRITABLE_RESOURCE_GRAPHQL = 'writable_resource_graphql',
   MISSING_URL_CONTEXT_TELEMETRY = 'missing_url_context_telemetry',
+  USER_ACCOUNT_TRACE_HEADER = 'user_account_trace_header',
 }
 
 const enabledExperimentalFeatures: Set<ExperimentalFeature> = new Set()
diff --git a/packages/logs/src/domain/contexts/commonContext.ts b/packages/logs/src/domain/contexts/commonContext.ts
index 91555315dc..6259d0ac34 100644
--- a/packages/logs/src/domain/contexts/commonContext.ts
+++ b/packages/logs/src/domain/contexts/commonContext.ts
@@ -4,7 +4,7 @@ import type { CommonContext } from '../../rawLogsEvent.types'
 export function buildCommonContext(
   globalContextManager: ContextManager,
   userContextManager: ContextManager,
-  acccountContextManager: ContextManager
+  accountContextManager: ContextManager
 ): CommonContext {
   return {
     view: {
@@ -13,6 +13,6 @@ export function buildCommonContext(
     },
     context: globalContextManager.getContext(),
     user: userContextManager.getContext(),
-    account: acccountContextManager.getContext(),
+    account: accountContextManager.getContext(),
   }
 }
diff --git a/packages/logs/src/rawLogsEvent.types.ts b/packages/logs/src/rawLogsEvent.types.ts
index 41c7d2dc19..0c896d4b85 100644
--- a/packages/logs/src/rawLogsEvent.types.ts
+++ b/packages/logs/src/rawLogsEvent.types.ts
@@ -68,5 +68,6 @@ export interface CommonContext {
   }
   context: Context
   user: User
+  // We don't want to enforce id internally so use Context as internal type
   account: Context
 }
diff --git a/packages/rum-core/src/boot/rumPublicApi.ts b/packages/rum-core/src/boot/rumPublicApi.ts
index aada551b3b..1ee6b35bb3 100644
--- a/packages/rum-core/src/boot/rumPublicApi.ts
+++ b/packages/rum-core/src/boot/rumPublicApi.ts
@@ -419,7 +419,7 @@ export function makeRumPublicApi(
     },
   })
   const accountContextManager = createContextManager('account', {
-    customerDataTracker: customerDataTrackerManager.getOrCreateTracker(CustomerDataType.User),
+    customerDataTracker: customerDataTrackerManager.getOrCreateTracker(CustomerDataType.Account),
     propertiesConfig: {
       id: { type: 'string', required: true },
       name: { type: 'string' },
diff --git a/packages/rum-core/src/boot/startRum.ts b/packages/rum-core/src/boot/startRum.ts
index 81fff14f97..8a4a67993d 100644
--- a/packages/rum-core/src/boot/startRum.ts
+++ b/packages/rum-core/src/boot/startRum.ts
@@ -198,7 +198,7 @@ export function startRum(
 
   const { addError } = startErrorCollection(lifeCycle, configuration)
 
-  startRequestCollection(lifeCycle, configuration, session)
+  startRequestCollection(lifeCycle, configuration, session, getCommonContext)
 
   const vitalCollection = startVitalCollection(lifeCycle, pageStateHistory, customVitalsState)
   const internalContext = startInternalContext(
diff --git a/packages/rum-core/src/domain/contexts/commonContext.ts b/packages/rum-core/src/domain/contexts/commonContext.ts
index 82c2e705ab..c6ff89f70e 100644
--- a/packages/rum-core/src/domain/contexts/commonContext.ts
+++ b/packages/rum-core/src/domain/contexts/commonContext.ts
@@ -3,6 +3,7 @@ import type { RecorderApi } from '../../boot/rumPublicApi'
 
 export interface CommonContext {
   user: User
+  // We don't want to enforce id internally so use Context as internal type
   account: Context
   context: Context
   hasReplay: true | undefined
diff --git a/packages/rum-core/src/domain/requestCollection.ts b/packages/rum-core/src/domain/requestCollection.ts
index a2772b8050..788322d630 100644
--- a/packages/rum-core/src/domain/requestCollection.ts
+++ b/packages/rum-core/src/domain/requestCollection.ts
@@ -23,6 +23,7 @@ import { isAllowedRequestUrl } from './resource/resourceUtils'
 import type { Tracer } from './tracing/tracer'
 import { startTracer } from './tracing/tracer'
 import type { SpanIdentifier, TraceIdentifier } from './tracing/identifier'
+import type { CommonContext } from './contexts/commonContext'
 
 export interface CustomContext {
   requestIndex: number
@@ -65,9 +66,10 @@ let nextRequestIndex = 1
 export function startRequestCollection(
   lifeCycle: LifeCycle,
   configuration: RumConfiguration,
-  sessionManager: RumSessionManager
+  sessionManager: RumSessionManager,
+  getCommonContext: () => CommonContext
 ) {
-  const tracer = startTracer(configuration, sessionManager)
+  const tracer = startTracer(configuration, sessionManager, getCommonContext)
   trackXhr(lifeCycle, configuration, tracer)
   trackFetch(lifeCycle, tracer)
 }
diff --git a/packages/rum-core/src/domain/tracing/encodedContext.spec.ts b/packages/rum-core/src/domain/tracing/encodedContext.spec.ts
new file mode 100644
index 0000000000..d2d8da79f6
--- /dev/null
+++ b/packages/rum-core/src/domain/tracing/encodedContext.spec.ts
@@ -0,0 +1,41 @@
+import { encodedContextCache, encodeToUtf8Base64, getEncodedContext } from './encodedContext'
+
+describe('encodeToUtf8Base64', () => {
+  it('should encode non-ascii to base64 in utf8', () => {
+    // https://developer.mozilla.org/en-US/docs/Web/API/Window/btoa
+    const encoded = encodeToUtf8Base64('a Ā 𐀀 文 🦄')
+    expect(encoded).toBe('YSDEgCDwkICAIOaWhyDwn6aE')
+  })
+})
+
+describe('encodedContextCache', () => {
+  encodedContextCache.set('text', 'encoded')
+  it('should return undefined when input is empty or undefined', () => {
+    const btoaSpy = spyOn(window, 'btoa').and.callThrough()
+
+    expect(getEncodedContext(undefined)).toBeUndefined()
+    expect(getEncodedContext('')).toBeUndefined()
+    expect(btoaSpy).not.toHaveBeenCalled()
+  })
+
+  it('should return undefined if cache key is not a string', () => {
+    const btoaSpy = spyOn(window, 'btoa').and.callThrough()
+
+    expect(getEncodedContext(42 as any)).toBeUndefined()
+    expect(btoaSpy).not.toHaveBeenCalled()
+  })
+
+  it('should use cache when context is the same', () => {
+    const btoaSpy = spyOn(window, 'btoa').and.callThrough()
+
+    getEncodedContext('text')
+    expect(btoaSpy).not.toHaveBeenCalled()
+  })
+
+  it('should update cache when context is different', () => {
+    const btoaSpy = spyOn(window, 'btoa').and.callThrough()
+
+    getEncodedContext('text2')
+    expect(btoaSpy).toHaveBeenCalled()
+  })
+})
diff --git a/packages/rum-core/src/domain/tracing/encodedContext.ts b/packages/rum-core/src/domain/tracing/encodedContext.ts
new file mode 100644
index 0000000000..774486fcee
--- /dev/null
+++ b/packages/rum-core/src/domain/tracing/encodedContext.ts
@@ -0,0 +1,25 @@
+export const encodedContextCache: Map<unknown, string> = new Map()
+
+export function getEncodedContext(key: unknown): string | undefined {
+  if (!key || typeof key !== 'string') {
+    return undefined
+  }
+  if (encodedContextCache.has(key)) {
+    return encodedContextCache.get(key)
+  }
+  const encoded = encodeToUtf8Base64(key)
+  encodedContextCache.set(key, encoded)
+  return encoded
+}
+
+/**
+ *
+ * Helper function to encode a string to base64 in UTF-8
+ * Comply with the `_dd.p.usr` standard and avoid non-ASCII characters
+ * https://developer.mozilla.org/en-US/docs/Web/API/Window/btoa
+ */
+export function encodeToUtf8Base64(plainText: string) {
+  const bytes = new TextEncoder().encode(plainText)
+  const binString = String.fromCodePoint(...bytes)
+  return btoa(binString)
+}
diff --git a/packages/rum-core/src/domain/tracing/tracer.spec.ts b/packages/rum-core/src/domain/tracing/tracer.spec.ts
index a959f73a51..996873e6f5 100644
--- a/packages/rum-core/src/domain/tracing/tracer.spec.ts
+++ b/packages/rum-core/src/domain/tracing/tracer.spec.ts
@@ -1,13 +1,22 @@
-import { display, objectEntries, TraceContextInjection } from '@datadog/browser-core'
+import { display, ExperimentalFeature, objectEntries, TraceContextInjection } from '@datadog/browser-core'
+import { mockExperimentalFeatures } from '../../../../core/test'
 import type { RumSessionManagerMock } from '../../../test'
 import { createRumSessionManagerMock } from '../../../test'
 import type { RumFetchResolveContext, RumFetchStartContext, RumXhrStartContext } from '../requestCollection'
 import type { RumConfiguration, RumInitConfiguration } from '../configuration'
 import { validateAndBuildRumConfiguration } from '../configuration'
+import type { CommonContext } from '../contexts/commonContext'
 import { startTracer } from './tracer'
 import type { SpanIdentifier, TraceIdentifier } from './identifier'
 import { createSpanIdentifier, createTraceIdentifier } from './identifier'
 
+const mockGetCommonContext = jasmine.createSpy().and.returnValue({
+  user: {
+    id: '1234',
+  },
+  account: { id: '5678' },
+} as unknown as CommonContext)
+
 describe('tracer', () => {
   let configuration: RumConfiguration
   const ALLOWED_DOMAIN_CONTEXT: Partial<RumXhrStartContext | RumFetchStartContext> = {
@@ -47,7 +56,7 @@ describe('tracer', () => {
     })
 
     it('should add traceId and spanId to context and add tracing headers', () => {
-      const tracer = startTracer(configuration, sessionManager)
+      const tracer = startTracer(configuration, sessionManager, mockGetCommonContext)
       const context = { ...ALLOWED_DOMAIN_CONTEXT }
       tracer.traceXhr(context, xhr as unknown as XMLHttpRequest)
 
@@ -57,7 +66,7 @@ describe('tracer', () => {
     })
 
     it('should not trace request on disallowed domain', () => {
-      const tracer = startTracer(configuration, sessionManager)
+      const tracer = startTracer(configuration, sessionManager, mockGetCommonContext)
       const context = { ...DISALLOWED_DOMAIN_CONTEXT }
       tracer.traceXhr(context, xhr as unknown as XMLHttpRequest)
 
@@ -67,7 +76,7 @@ describe('tracer', () => {
     })
 
     it('should not trace request during untracked session', () => {
-      const tracer = startTracer(configuration, sessionManager.setNotTracked())
+      const tracer = startTracer(configuration, sessionManager.setNotTracked(), mockGetCommonContext)
       const context = { ...ALLOWED_DOMAIN_CONTEXT }
       tracer.traceXhr(context, xhr as unknown as XMLHttpRequest)
 
@@ -77,7 +86,7 @@ describe('tracer', () => {
     })
 
     it("should trace request with priority '1' when sampled", () => {
-      const tracer = startTracer({ ...configuration, traceSampleRate: 100 }, sessionManager)
+      const tracer = startTracer({ ...configuration, traceSampleRate: 100 }, sessionManager, mockGetCommonContext)
       const context = { ...ALLOWED_DOMAIN_CONTEXT }
       tracer.traceXhr(context, xhr as unknown as XMLHttpRequest)
 
@@ -90,7 +99,8 @@ describe('tracer', () => {
     it("should trace request with priority '0' when not sampled and config set to all", () => {
       const tracer = startTracer(
         { ...configuration, traceSampleRate: 0, traceContextInjection: TraceContextInjection.ALL },
-        sessionManager
+        sessionManager,
+        mockGetCommonContext
       )
       const context = { ...ALLOWED_DOMAIN_CONTEXT }
       tracer.traceXhr(context, xhr as unknown as XMLHttpRequest)
@@ -109,7 +119,7 @@ describe('tracer', () => {
         allowedTracingUrls: [{ match: window.location.origin, propagatorTypes: ['b3', 'tracecontext', 'b3multi'] }],
       })!
 
-      const tracer = startTracer(configurationWithAllOtelHeaders, sessionManager)
+      const tracer = startTracer(configurationWithAllOtelHeaders, sessionManager, mockGetCommonContext)
       const context = { ...ALLOWED_DOMAIN_CONTEXT }
       tracer.traceXhr(context, xhr as unknown as XMLHttpRequest)
 
@@ -134,7 +144,7 @@ describe('tracer', () => {
       })!
       const stub = xhr as unknown as XMLHttpRequest
 
-      const tracer = startTracer(configurationWithTracingUrls, sessionManager)
+      const tracer = startTracer(configurationWithTracingUrls, sessionManager, mockGetCommonContext)
 
       let context: Partial<RumXhrStartContext> = { url: 'http://qux.com' }
       tracer.traceXhr(context, stub)
@@ -158,7 +168,7 @@ describe('tracer', () => {
         allowedTracingUrls: [{ match: window.location.origin, propagatorTypes: ['b3multi'] }],
       })!
 
-      const tracer = startTracer(configurationWithb3multi, sessionManager)
+      const tracer = startTracer(configurationWithb3multi, sessionManager, mockGetCommonContext)
       const context = { ...ALLOWED_DOMAIN_CONTEXT }
       tracer.traceXhr(context, xhr as unknown as XMLHttpRequest)
 
@@ -182,7 +192,7 @@ describe('tracer', () => {
         allowedTracingUrls: [{ match: window.location.origin, propagatorTypes: ['b3', 'tracecontext'] }],
       })!
 
-      const tracer = startTracer(configurationWithB3andTracecontext, sessionManager)
+      const tracer = startTracer(configurationWithB3andTracecontext, sessionManager, mockGetCommonContext)
       const context = { ...ALLOWED_DOMAIN_CONTEXT }
       tracer.traceXhr(context, xhr as unknown as XMLHttpRequest)
 
@@ -201,7 +211,7 @@ describe('tracer', () => {
         allowedTracingUrls: [{ match: window.location.origin, propagatorTypes: [] }],
       })!
 
-      const tracer = startTracer(configurationWithoutHeaders, sessionManager)
+      const tracer = startTracer(configurationWithoutHeaders, sessionManager, mockGetCommonContext)
       const context = { ...ALLOWED_DOMAIN_CONTEXT }
       tracer.traceXhr(context, xhr as unknown as XMLHttpRequest)
 
@@ -218,7 +228,7 @@ describe('tracer', () => {
         traceSampleRate: 0,
       }
 
-      const tracer = startTracer(configurationWithInjectionParam, sessionManager)
+      const tracer = startTracer(configurationWithInjectionParam, sessionManager, mockGetCommonContext)
       const context = { ...ALLOWED_DOMAIN_CONTEXT }
       tracer.traceXhr(context, xhr as unknown as XMLHttpRequest)
 
@@ -232,7 +242,7 @@ describe('tracer', () => {
         traceSampleRate: 100,
       }
 
-      const tracer = startTracer(configurationWithInjectionParam, sessionManager)
+      const tracer = startTracer(configurationWithInjectionParam, sessionManager, mockGetCommonContext)
       const context = { ...ALLOWED_DOMAIN_CONTEXT }
       tracer.traceXhr(context, xhr as unknown as XMLHttpRequest)
 
@@ -247,7 +257,7 @@ describe('tracer', () => {
         traceContextInjection: TraceContextInjection.ALL,
       }
 
-      const tracer = startTracer(configurationWithInjectionParam, sessionManager)
+      const tracer = startTracer(configurationWithInjectionParam, sessionManager, mockGetCommonContext)
       const context = { ...ALLOWED_DOMAIN_CONTEXT }
       tracer.traceXhr(context, xhr as unknown as XMLHttpRequest)
 
@@ -255,13 +265,31 @@ describe('tracer', () => {
       expect(xhr.headers['x-datadog-sampling-priority']).toBeDefined()
     })
 
+    it('should add usr.id and account.id to tracestate when feature is disabled', () => {
+      mockExperimentalFeatures([ExperimentalFeature.USER_ACCOUNT_TRACE_HEADER])
+      const configurationWithB3andTracecontext = validateAndBuildRumConfiguration({
+        ...INIT_CONFIGURATION,
+        allowedTracingUrls: [{ match: window.location.origin, propagatorTypes: ['tracecontext'] }],
+      })!
+
+      const tracer = startTracer(configurationWithB3andTracecontext, sessionManager, mockGetCommonContext)
+      const context = { ...ALLOWED_DOMAIN_CONTEXT }
+      tracer.traceXhr(context, xhr as unknown as XMLHttpRequest)
+      expect(xhr.headers).toEqual(
+        jasmine.objectContaining({
+          traceparent: jasmine.stringMatching(/^[0-9a-f]{2}-[0-9a-f]{32}-[0-9a-f]{16}-01$/),
+          tracestate: 'dd=s:1;o:rum;t.usr.id:MTIzNA==;t.account.id:NTY3OA==',
+        })
+      )
+    })
+
     it('should ignore wrong propagator types', () => {
       const configurationWithBadParams = validateAndBuildRumConfiguration({
         ...INIT_CONFIGURATION,
         allowedTracingUrls: [{ match: window.location.origin, propagatorTypes: ['foo', 32, () => true] as any }],
       })!
 
-      const tracer = startTracer(configurationWithBadParams, sessionManager)
+      const tracer = startTracer(configurationWithBadParams, sessionManager, mockGetCommonContext)
       const context = { ...ALLOWED_DOMAIN_CONTEXT }
       tracer.traceXhr(context, xhr as unknown as XMLHttpRequest)
 
@@ -283,7 +311,7 @@ describe('tracer', () => {
         ],
       })!
 
-      const tracer = startTracer(configurationWithBadParams, sessionManager)
+      const tracer = startTracer(configurationWithBadParams, sessionManager, mockGetCommonContext)
       const context = { ...ALLOWED_DOMAIN_CONTEXT }
       tracer.traceXhr(context, xhr as unknown as XMLHttpRequest)
 
@@ -294,7 +322,7 @@ describe('tracer', () => {
   describe('traceFetch', () => {
     it('should add traceId and spanId to context, and add tracing headers', () => {
       const context: Partial<RumFetchStartContext> = { ...ALLOWED_DOMAIN_CONTEXT }
-      const tracer = startTracer(configuration, sessionManager)
+      const tracer = startTracer(configuration, sessionManager, mockGetCommonContext)
       tracer.traceFetch(context)
 
       expect(context.traceId).toBeDefined()
@@ -309,7 +337,7 @@ describe('tracer', () => {
         init,
       }
 
-      const tracer = startTracer(configuration, sessionManager)
+      const tracer = startTracer(configuration, sessionManager, mockGetCommonContext)
       tracer.traceFetch(context)
 
       expect(context.init).not.toBe(init)
@@ -326,7 +354,7 @@ describe('tracer', () => {
         init: { headers, method: 'POST' },
       }
 
-      const tracer = startTracer(configuration, sessionManager)
+      const tracer = startTracer(configuration, sessionManager, mockGetCommonContext)
       tracer.traceFetch(context)
 
       expect(context.init!.headers).not.toBe(headers)
@@ -347,7 +375,7 @@ describe('tracer', () => {
         init: { headers, method: 'POST' },
       }
 
-      const tracer = startTracer(configuration, sessionManager)
+      const tracer = startTracer(configuration, sessionManager, mockGetCommonContext)
       tracer.traceFetch(context)
 
       expect(context.init!.headers).not.toBe(headers)
@@ -372,7 +400,7 @@ describe('tracer', () => {
         init: { headers, method: 'POST' },
       }
 
-      const tracer = startTracer(configuration, sessionManager)
+      const tracer = startTracer(configuration, sessionManager, mockGetCommonContext)
       tracer.traceFetch(context)
 
       expect(context.init!.headers).not.toBe(headers)
@@ -400,7 +428,7 @@ describe('tracer', () => {
         input: request,
       }
 
-      const tracer = startTracer(configuration, sessionManager)
+      const tracer = startTracer(configuration, sessionManager, mockGetCommonContext)
       tracer.traceFetch(context)
 
       expect(context.init).toBe(undefined)
@@ -421,7 +449,7 @@ describe('tracer', () => {
         }),
       }
 
-      const tracer = startTracer(configuration, sessionManager)
+      const tracer = startTracer(configuration, sessionManager, mockGetCommonContext)
       tracer.traceFetch(context)
 
       expect(context.init!.headers).toEqual([
@@ -433,7 +461,7 @@ describe('tracer', () => {
     it('should not trace request on disallowed domain', () => {
       const context: Partial<RumFetchStartContext> = { ...DISALLOWED_DOMAIN_CONTEXT }
 
-      const tracer = startTracer(configuration, sessionManager)
+      const tracer = startTracer(configuration, sessionManager, mockGetCommonContext)
       tracer.traceFetch(context)
 
       expect(context.traceId).toBeUndefined()
@@ -444,7 +472,7 @@ describe('tracer', () => {
     it('should not trace request during untracked session', () => {
       const context: Partial<RumFetchStartContext> = { ...ALLOWED_DOMAIN_CONTEXT }
 
-      const tracer = startTracer(configuration, sessionManager.setNotTracked())
+      const tracer = startTracer(configuration, sessionManager.setNotTracked(), mockGetCommonContext)
       tracer.traceFetch(context)
 
       expect(context.traceId).toBeUndefined()
@@ -455,7 +483,7 @@ describe('tracer', () => {
     it("should trace request with priority '1' when sampled", () => {
       const context: Partial<RumFetchStartContext> = { ...ALLOWED_DOMAIN_CONTEXT }
 
-      const tracer = startTracer({ ...configuration, traceSampleRate: 100 }, sessionManager)
+      const tracer = startTracer({ ...configuration, traceSampleRate: 100 }, sessionManager, mockGetCommonContext)
       tracer.traceFetch(context)
 
       expect(context.traceSampled).toBe(true)
@@ -469,7 +497,8 @@ describe('tracer', () => {
 
       const tracer = startTracer(
         { ...configuration, traceSampleRate: 0, traceContextInjection: TraceContextInjection.ALL },
-        sessionManager
+        sessionManager,
+        mockGetCommonContext
       )
       tracer.traceFetch(context)
 
@@ -492,7 +521,7 @@ describe('tracer', () => {
       const barDomainContext: Partial<RumFetchStartContext> = { url: 'http://bar.com' }
       const dynamicDomainContext: Partial<RumFetchStartContext> = { url: 'http://dynamic.com' }
 
-      const tracer = startTracer(configurationWithTracingUrls, sessionManager)
+      const tracer = startTracer(configurationWithTracingUrls, sessionManager, mockGetCommonContext)
 
       tracer.traceFetch(quxDomainContext)
       tracer.traceFetch(barDomainContext)
@@ -511,7 +540,7 @@ describe('tracer', () => {
         allowedTracingUrls: [{ match: window.location.origin, propagatorTypes: ['b3multi'] }],
       })!
 
-      const tracer = startTracer(configurationWithb3multi, sessionManager)
+      const tracer = startTracer(configurationWithb3multi, sessionManager, mockGetCommonContext)
       const context: Partial<RumFetchStartContext> = { ...ALLOWED_DOMAIN_CONTEXT }
       tracer.traceFetch(context)
 
@@ -539,7 +568,7 @@ describe('tracer', () => {
         allowedTracingUrls: [{ match: window.location.origin, propagatorTypes: ['b3', 'tracecontext'] }],
       })!
 
-      const tracer = startTracer(configurationWithB3andTracecontext, sessionManager)
+      const tracer = startTracer(configurationWithB3andTracecontext, sessionManager, mockGetCommonContext)
       const context: Partial<RumFetchStartContext> = { ...ALLOWED_DOMAIN_CONTEXT }
       tracer.traceFetch(context)
 
@@ -558,7 +587,7 @@ describe('tracer', () => {
         allowedTracingUrls: [{ match: window.location.origin, propagatorTypes: [] }],
       })!
 
-      const tracer = startTracer(configurationWithoutHeaders, sessionManager)
+      const tracer = startTracer(configurationWithoutHeaders, sessionManager, mockGetCommonContext)
       const context: Partial<RumFetchStartContext> = { ...ALLOWED_DOMAIN_CONTEXT }
       tracer.traceFetch(context)
 
@@ -575,7 +604,7 @@ describe('tracer', () => {
         traceContextInjection: TraceContextInjection.SAMPLED,
       })!
 
-      const tracer = startTracer(configurationWithHeaders, sessionManager)
+      const tracer = startTracer(configurationWithHeaders, sessionManager, mockGetCommonContext)
       const context: Partial<RumFetchStartContext> = { ...ALLOWED_DOMAIN_CONTEXT }
       tracer.traceFetch(context)
 
@@ -589,7 +618,7 @@ describe('tracer', () => {
         traceContextInjection: TraceContextInjection.SAMPLED,
       })!
 
-      const tracer = startTracer(configurationWithHeaders, sessionManager)
+      const tracer = startTracer(configurationWithHeaders, sessionManager, mockGetCommonContext)
       const context: Partial<RumFetchStartContext> = { ...ALLOWED_DOMAIN_CONTEXT }
       tracer.traceFetch(context)
 
@@ -606,7 +635,7 @@ describe('tracer', () => {
         traceContextInjection: TraceContextInjection.ALL,
       })!
 
-      const tracer = startTracer(configurationWithHeaders, sessionManager)
+      const tracer = startTracer(configurationWithHeaders, sessionManager, mockGetCommonContext)
       const context: Partial<RumFetchStartContext> = { ...ALLOWED_DOMAIN_CONTEXT }
       tracer.traceFetch(context)
 
@@ -619,7 +648,7 @@ describe('tracer', () => {
 
   describe('clearTracingIfCancelled', () => {
     it('should clear tracing if status is 0', () => {
-      const tracer = startTracer(configuration, sessionManager)
+      const tracer = startTracer(configuration, sessionManager, mockGetCommonContext)
       const context: RumFetchResolveContext = {
         status: 0,
 
@@ -633,7 +662,7 @@ describe('tracer', () => {
     })
 
     it('should not clear tracing if status is not 0', () => {
-      const tracer = startTracer(configuration, sessionManager)
+      const tracer = startTracer(configuration, sessionManager, mockGetCommonContext)
       const context: RumFetchResolveContext = {
         status: 200,
 
diff --git a/packages/rum-core/src/domain/tracing/tracer.ts b/packages/rum-core/src/domain/tracing/tracer.ts
index 318a570c8b..5a3d8c4d0e 100644
--- a/packages/rum-core/src/domain/tracing/tracer.ts
+++ b/packages/rum-core/src/domain/tracing/tracer.ts
@@ -5,6 +5,8 @@ import {
   isMatchOption,
   matchList,
   TraceContextInjection,
+  isExperimentalFeatureEnabled,
+  ExperimentalFeature,
 } from '@datadog/browser-core'
 import type { RumConfiguration } from '../configuration'
 import type {
@@ -13,11 +15,13 @@ import type {
   RumXhrCompleteContext,
   RumXhrStartContext,
 } from '../requestCollection'
+import type { CommonContext } from '../contexts/commonContext'
 import type { RumSessionManager } from '../rumSessionManager'
 import type { PropagatorType, TracingOption } from './tracer.types'
 import type { SpanIdentifier, TraceIdentifier } from './identifier'
 import { createSpanIdentifier, createTraceIdentifier, toPaddedHexadecimalString } from './identifier'
 import { isTraceSampled } from './sampler'
+import { getEncodedContext } from './encodedContext'
 
 export interface Tracer {
   traceFetch: (context: Partial<RumFetchStartContext>) => void
@@ -63,41 +67,57 @@ export function clearTracingIfNeeded(context: RumFetchResolveContext | RumXhrCom
   }
 }
 
-export function startTracer(configuration: RumConfiguration, sessionManager: RumSessionManager): Tracer {
+export function startTracer(
+  configuration: RumConfiguration,
+  sessionManager: RumSessionManager,
+  getCommonContext: () => CommonContext
+): Tracer {
   return {
     clearTracingIfNeeded,
     traceFetch: (context) =>
-      injectHeadersIfTracingAllowed(configuration, context, sessionManager, (tracingHeaders: TracingHeaders) => {
-        if (context.input instanceof Request && !context.init?.headers) {
-          context.input = new Request(context.input)
-          Object.keys(tracingHeaders).forEach((key) => {
-            ;(context.input as Request).headers.append(key, tracingHeaders[key])
-          })
-        } else {
-          context.init = shallowClone(context.init)
-          const headers: Array<[string, string]> = []
-          if (context.init.headers instanceof Headers) {
-            context.init.headers.forEach((value, key) => {
-              headers.push([key, value])
-            })
-          } else if (Array.isArray(context.init.headers)) {
-            context.init.headers.forEach((header) => {
-              headers.push(header)
-            })
-          } else if (context.init.headers) {
-            Object.keys(context.init.headers).forEach((key) => {
-              headers.push([key, (context.init!.headers as Record<string, string>)[key]])
+      injectHeadersIfTracingAllowed(
+        configuration,
+        context,
+        sessionManager,
+        getCommonContext,
+        (tracingHeaders: TracingHeaders) => {
+          if (context.input instanceof Request && !context.init?.headers) {
+            context.input = new Request(context.input)
+            Object.keys(tracingHeaders).forEach((key) => {
+              ;(context.input as Request).headers.append(key, tracingHeaders[key])
             })
+          } else {
+            context.init = shallowClone(context.init)
+            const headers: Array<[string, string]> = []
+            if (context.init.headers instanceof Headers) {
+              context.init.headers.forEach((value, key) => {
+                headers.push([key, value])
+              })
+            } else if (Array.isArray(context.init.headers)) {
+              context.init.headers.forEach((header) => {
+                headers.push(header)
+              })
+            } else if (context.init.headers) {
+              Object.keys(context.init.headers).forEach((key) => {
+                headers.push([key, (context.init!.headers as Record<string, string>)[key]])
+              })
+            }
+            context.init.headers = headers.concat(objectEntries(tracingHeaders))
           }
-          context.init.headers = headers.concat(objectEntries(tracingHeaders))
         }
-      }),
+      ),
     traceXhr: (context, xhr) =>
-      injectHeadersIfTracingAllowed(configuration, context, sessionManager, (tracingHeaders: TracingHeaders) => {
-        Object.keys(tracingHeaders).forEach((name) => {
-          xhr.setRequestHeader(name, tracingHeaders[name])
-        })
-      }),
+      injectHeadersIfTracingAllowed(
+        configuration,
+        context,
+        sessionManager,
+        getCommonContext,
+        (tracingHeaders: TracingHeaders) => {
+          Object.keys(tracingHeaders).forEach((name) => {
+            xhr.setRequestHeader(name, tracingHeaders[name])
+          })
+        }
+      ),
   }
 }
 
@@ -105,6 +125,7 @@ function injectHeadersIfTracingAllowed(
   configuration: RumConfiguration,
   context: Partial<RumFetchStartContext | RumXhrStartContext>,
   sessionManager: RumSessionManager,
+  getCommonContext: () => CommonContext,
   inject: (tracingHeaders: TracingHeaders) => void
 ) {
   const session = sessionManager.findTrackedSession()
@@ -130,7 +151,15 @@ function injectHeadersIfTracingAllowed(
   context.traceId = createTraceIdentifier()
   context.spanId = createSpanIdentifier()
 
-  inject(makeTracingHeaders(context.traceId, context.spanId, context.traceSampled, tracingOption.propagatorTypes))
+  inject(
+    makeTracingHeaders(
+      context.traceId,
+      context.spanId,
+      context.traceSampled,
+      tracingOption.propagatorTypes,
+      getCommonContext
+    )
+  )
 }
 
 /**
@@ -141,7 +170,8 @@ function makeTracingHeaders(
   traceId: TraceIdentifier,
   spanId: SpanIdentifier,
   traceSampled: boolean,
-  propagatorTypes: PropagatorType[]
+  propagatorTypes: PropagatorType[],
+  getCommonContext: () => CommonContext
 ): TracingHeaders {
   const tracingHeaders: TracingHeaders = {}
 
@@ -162,7 +192,7 @@ function makeTracingHeaders(
           traceparent: `00-0000000000000000${toPaddedHexadecimalString(traceId)}-${toPaddedHexadecimalString(spanId)}-0${
             traceSampled ? '1' : '0'
           }`,
-          tracestate: `dd=s:${traceSampled ? '1' : '0'};o:rum`,
+          tracestate: `dd=${getTraceStateDatadogItems(traceSampled, getCommonContext).join(';')}`,
         })
         break
       }
@@ -185,3 +215,21 @@ function makeTracingHeaders(
   })
   return tracingHeaders
 }
+
+function getTraceStateDatadogItems(traceSampled: boolean, getCommonContext: () => CommonContext): string[] {
+  const traceStateDatadogItems: string[] = [`s:${traceSampled ? '1' : '0'}`, 'o:rum']
+  if (isExperimentalFeatureEnabled(ExperimentalFeature.USER_ACCOUNT_TRACE_HEADER)) {
+    /**
+     * We should only enable this feature after the decoding service is available
+     */
+    const userIdEncoded = getEncodedContext(getCommonContext().user.id)
+    const accountIdEncoded = getEncodedContext(getCommonContext().account.id)
+    if (userIdEncoded) {
+      traceStateDatadogItems.push(`t.usr.id:${userIdEncoded}`)
+    }
+    if (accountIdEncoded) {
+      traceStateDatadogItems.push(`t.account.id:${accountIdEncoded}`)
+    }
+  }
+  return traceStateDatadogItems
+}


Reviews:
user: login: BenoitZugmeyer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-07T10:29:23Z
user: login: thomas-lebeau
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-07T11:04:43Z
user: login: cy-moi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T11:41:54Z
user: login: cy-moi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T11:44:24Z
user: login: thomas-lebeau
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-07T11:46:01Z
user: login: thomas-lebeau
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-07T11:49:38Z
user: login: cy-moi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T14:14:50Z
user: login: BenoitZugmeyer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-07T15:50:10Z
user: login: BenoitZugmeyer
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-10T09:26:22Z

Review comments:
diff_hunk: @@ -419,7 +419,7 @@ export function makeRumPublicApi(
     },
   })
   const accountContextManager = createContextManager('account', {
-    customerDataTracker: customerDataTrackerManager.getOrCreateTracker(CustomerDataType.User),
+    customerDataTracker: customerDataTrackerManager.getOrCreateTracker(CustomerDataType.Account),
path: packages/rum-core/src/boot/rumPublicApi.ts
user: login: BenoitZugmeyer
body: OH!
created_at: 2025-03-05T17:16:39Z
author_association: MEMBER
diff_hunk: @@ -185,3 +226,18 @@ function makeTracingHeaders(
   })
   return tracingHeaders
 }
+
+/**
+ *
+ * Helper function to encode a string to base64 in UTF-8
+ * Comply with the `_dd.p.usr` standard and avoid non-ASCII characters
+ * https://developer.mozilla.org/en-US/docs/Web/API/Window/btoa
+ */
+export function encodeToUtf8Base64(plainText: string | undefined) {
+  if (!plainText || plainText === '') {
+    return undefined
+  }
+  const bytes = new TextEncoder().encode(plainText)
+  const binString = Array.from(bytes, (byte) => String.fromCodePoint(byte)).join('')
+  return btoa(binString)
path: packages/rum-core/src/domain/tracing/tracer.ts
user: login: BenoitZugmeyer
body: **💬 suggestion:** this might be simpler:‏
```suggestion
  const bytes = new TextEncoder().encode(plainText)
  const binString = String.fromCodePoint(...bytes)
  return btoa(binString)
```
created_at: 2025-03-07T09:16:31Z
author_association: MEMBER
diff_hunk: @@ -177,15 +189,34 @@ describe('tracer', () => {
     })
 
     it('should add headers for B3 (single) and tracecontext propagators', () => {
+      mockExperimentalFeatures([ExperimentalFeature.USER_ACCOUNT_TRACE_HEADER])
+
       const configurationWithB3andTracecontext = validateAndBuildRumConfiguration({
         ...INIT_CONFIGURATION,
         allowedTracingUrls: [{ match: window.location.origin, propagatorTypes: ['b3', 'tracecontext'] }],
       })!
 
-      const tracer = startTracer(configurationWithB3andTracecontext, sessionManager)
+      const tracer = startTracer(configurationWithB3andTracecontext, sessionManager, mockGetCommonContext)
       const context = { ...ALLOWED_DOMAIN_CONTEXT }
       tracer.traceXhr(context, xhr as unknown as XMLHttpRequest)
 
+      expect(xhr.headers).toEqual(
+        jasmine.objectContaining({
+          b3: jasmine.stringMatching(/^[0-9a-f]{16}-[0-9a-f]{16}-1$/),
+          traceparent: jasmine.stringMatching(/^[0-9a-f]{2}-[0-9a-f]{32}-[0-9a-f]{16}-01$/),
+          tracestate: 'dd=t.usr.id:MTIzNA==;t.account.id:NTY3OA==;s:1;o:rum',
+        })
+      )
+    })
path: packages/rum-core/src/domain/tracing/tracer.spec.ts
user: login: BenoitZugmeyer
body: **💬 suggestion:** ‏instead of re-using this test case to check for usr.id/account.id, maybe add a new, more focused one? (i.e. we don't care about b3)
created_at: 2025-03-07T09:27:26Z
author_association: MEMBER
diff_hunk: @@ -177,15 +189,34 @@ describe('tracer', () => {
     })
 
     it('should add headers for B3 (single) and tracecontext propagators', () => {
+      mockExperimentalFeatures([ExperimentalFeature.USER_ACCOUNT_TRACE_HEADER])
path: packages/rum-core/src/domain/tracing/tracer.spec.ts
user: login: BenoitZugmeyer
body: **🥜 nitpick:** ‏mention that we are using the flag in the test title
created_at: 2025-03-07T09:27:42Z
author_association: MEMBER
diff_hunk: @@ -158,11 +187,23 @@ function makeTracingHeaders(
       }
       // https://www.w3.org/TR/trace-context/
       case 'tracecontext': {
+        let userAccountTraceHeader: string | undefined
+        if (isExperimentalFeatureEnabled(ExperimentalFeature.USER_ACCOUNT_TRACE_HEADER)) {
+          /**
+           * We should only enable this feature after the decoding service is available
+           */
+          const userIdEncoded = encodeToUtf8Base64(getCommonContext().user.id?.toString())
+          // we use Context type for account internally but account id should not be an object
+          /* eslint-disable-next-line @typescript-eslint/no-base-to-string */
+          const accountIdEncoded = encodeToUtf8Base64(getCommonContext().account.id?.toString())
path: packages/rum-core/src/domain/tracing/tracer.ts
user: login: BenoitZugmeyer
body: **💬 suggestion:** ‏user.id and account.id are enforced to be strings, so in practice `toString()` isn't needed. But typescript does not that. What about:

* removing `toString()` here
* treat those as "unknown" in `encodeToUtf8Base64`?


created_at: 2025-03-07T10:19:02Z
author_association: MEMBER
diff_hunk: @@ -185,3 +226,18 @@ function makeTracingHeaders(
   })
   return tracingHeaders
 }
+
+/**
+ *
+ * Helper function to encode a string to base64 in UTF-8
+ * Comply with the `_dd.p.usr` standard and avoid non-ASCII characters
+ * https://developer.mozilla.org/en-US/docs/Web/API/Window/btoa
+ */
+export function encodeToUtf8Base64(plainText: string | undefined) {
+  if (!plainText || plainText === '') {
+    return undefined
+  }
path: packages/rum-core/src/domain/tracing/tracer.ts
user: login: BenoitZugmeyer
body: **💬 suggestion:** ‏(follow-up of my previous suggestion)

```suggestion
export function encodeToUtf8Base64(plainText: unknown) {
  if (!plainText || typeof plainText !== 'string') {
    return undefined
  }
```
created_at: 2025-03-07T10:19:50Z
author_association: MEMBER
diff_hunk: @@ -158,11 +187,23 @@ function makeTracingHeaders(
       }
       // https://www.w3.org/TR/trace-context/
       case 'tracecontext': {
+        let userAccountTraceHeader: string | undefined
+        if (isExperimentalFeatureEnabled(ExperimentalFeature.USER_ACCOUNT_TRACE_HEADER)) {
+          /**
+           * We should only enable this feature after the decoding service is available
+           */
+          const userIdEncoded = encodeToUtf8Base64(getCommonContext().user.id?.toString())
+          // we use Context type for account internally but account id should not be an object
+          /* eslint-disable-next-line @typescript-eslint/no-base-to-string */
+          const accountIdEncoded = encodeToUtf8Base64(getCommonContext().account.id?.toString())
+          userAccountTraceHeader = `${userIdEncoded ? `t.usr.id:${userIdEncoded};` : ''}${accountIdEncoded ? `t.account.id:${accountIdEncoded};` : ''}`
+        }
+
         Object.assign(tracingHeaders, {
           traceparent: `00-0000000000000000${toPaddedHexadecimalString(traceId)}-${toPaddedHexadecimalString(spanId)}-0${
             traceSampled ? '1' : '0'
           }`,
-          tracestate: `dd=s:${traceSampled ? '1' : '0'};o:rum`,
+          tracestate: `dd=${userAccountTraceHeader ?? ''}s:${traceSampled ? '1' : '0'};o:rum`,
path: packages/rum-core/src/domain/tracing/tracer.ts
user: login: BenoitZugmeyer
body: **💬 suggestion:** ‏I think it could be nice have an array of strings that we join in the end
```suggestion
          tracestate: `dd=${traceStateDatadogItems.join(';')`,
```

so you can just do:
```ts
if (userIdEncoded) {
  traceStateDatadogItems.push(`t.usr.id:${userIdEncoded}`)
}
```

and so on and avoid a bunch of string manipulation.

Note: @xgouchet is working on adding the session id to that header, so he would also push it into this array.
created_at: 2025-03-07T10:28:13Z
author_association: MEMBER
diff_hunk: @@ -68,5 +68,5 @@ export interface CommonContext {
   }
   context: Context
   user: User
-  account: Context
+  account: Context // use Context for internal Account type
path: packages/logs/src/rawLogsEvent.types.ts
user: login: thomas-lebeau
body: <!-- Suggestions are specific requests to improve the subject under review. It is assumed that we all want to do what's best, so these comments are never dismissed as “mere suggestions”, but are taken seriously.  -->
**💬 suggestion:** ‏ what about using `Account` or `AccountContext` type and define it as `type Account = Context` if they are really the same

Also, why are we using `Context`?
created_at: 2025-03-07T10:58:43Z
author_association: COLLABORATOR
diff_hunk: @@ -3,7 +3,7 @@ import type { RecorderApi } from '../../boot/rumPublicApi'
 
 export interface CommonContext {
   user: User
-  account: Context
+  account: Context // Use Context as internal Account type
path: packages/rum-core/src/domain/contexts/commonContext.ts
user: login: thomas-lebeau
body: <!-- Suggestions are specific requests to improve the subject under review. It is assumed that we all want to do what's best, so these comments are never dismissed as “mere suggestions”, but are taken seriously.  -->
**💬 suggestion:** ‏ same here!
created_at: 2025-03-07T10:59:26Z
author_association: COLLABORATOR
diff_hunk: @@ -158,11 +187,23 @@ function makeTracingHeaders(
       }
       // https://www.w3.org/TR/trace-context/
       case 'tracecontext': {
+        let userAccountTraceHeader: string | undefined
+        if (isExperimentalFeatureEnabled(ExperimentalFeature.USER_ACCOUNT_TRACE_HEADER)) {
+          /**
+           * We should only enable this feature after the decoding service is available
+           */
+          const userIdEncoded = encodeToUtf8Base64(getCommonContext().user.id?.toString())
+          // we use Context type for account internally but account id should not be an object
+          /* eslint-disable-next-line @typescript-eslint/no-base-to-string */
+          const accountIdEncoded = encodeToUtf8Base64(getCommonContext().account.id?.toString())
path: packages/rum-core/src/domain/tracing/tracer.ts
user: login: thomas-lebeau
body: <!-- Thoughts represent an idea that popped up from reviewing. These comments are non-blocking by nature, but they are extremely valuable and can lead to more focused initiatives and mentoring opportunities.  -->
**💭 thought:** ‏ I don't know if the performance overhead is significant, but as user.id and account.id are most probably the same for all traces, this could be easily cached/memoized. wdyt?
created_at: 2025-03-07T11:04:32Z
author_association: COLLABORATOR
diff_hunk: @@ -68,5 +68,5 @@ export interface CommonContext {
   }
   context: Context
   user: User
-  account: Context
+  account: Context // use Context for internal Account type
path: packages/logs/src/rawLogsEvent.types.ts
user: login: cy-moi
body: I think this was a decision made in the previous PR ([link](https://github.com/DataDog/browser-sdk/pull/3242#discussion_r1913481873)). I added the comments just to make this clear. It took me a while to understand why we do this as well.
created_at: 2025-03-07T11:41:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -158,11 +187,23 @@ function makeTracingHeaders(
       }
       // https://www.w3.org/TR/trace-context/
       case 'tracecontext': {
+        let userAccountTraceHeader: string | undefined
+        if (isExperimentalFeatureEnabled(ExperimentalFeature.USER_ACCOUNT_TRACE_HEADER)) {
+          /**
+           * We should only enable this feature after the decoding service is available
+           */
+          const userIdEncoded = encodeToUtf8Base64(getCommonContext().user.id?.toString())
+          // we use Context type for account internally but account id should not be an object
+          /* eslint-disable-next-line @typescript-eslint/no-base-to-string */
+          const accountIdEncoded = encodeToUtf8Base64(getCommonContext().account.id?.toString())
path: packages/rum-core/src/domain/tracing/tracer.ts
user: login: cy-moi
body: Good point. They are from context manager, so I assume we already do some caching here? 

There is also the Kiosk use case where these fields are expected to change frequently.
created_at: 2025-03-07T11:44:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,5 +68,5 @@ export interface CommonContext {
   }
   context: Context
   user: User
-  account: Context
+  account: Context // use Context for internal Account type
path: packages/logs/src/rawLogsEvent.types.ts
user: login: thomas-lebeau
body: hmm, ok. Maybe you can add _why_ in the comment, so the next person to read that will understand too.

Or maybe naming `AccountWithId` would help
created_at: 2025-03-07T11:46:01Z
author_association: COLLABORATOR
diff_hunk: @@ -158,11 +187,23 @@ function makeTracingHeaders(
       }
       // https://www.w3.org/TR/trace-context/
       case 'tracecontext': {
+        let userAccountTraceHeader: string | undefined
+        if (isExperimentalFeatureEnabled(ExperimentalFeature.USER_ACCOUNT_TRACE_HEADER)) {
+          /**
+           * We should only enable this feature after the decoding service is available
+           */
+          const userIdEncoded = encodeToUtf8Base64(getCommonContext().user.id?.toString())
+          // we use Context type for account internally but account id should not be an object
+          /* eslint-disable-next-line @typescript-eslint/no-base-to-string */
+          const accountIdEncoded = encodeToUtf8Base64(getCommonContext().account.id?.toString())
path: packages/rum-core/src/domain/tracing/tracer.ts
user: login: thomas-lebeau
body: I meant caching the encoded values, so we don't run the encoding algorithm for every requests.
Even in the case of the Kiosk, there is most likely multiple request in a row with the same ids
created_at: 2025-03-07T11:49:38Z
author_association: COLLABORATOR
diff_hunk: @@ -68,5 +68,5 @@ export interface CommonContext {
   }
   context: Context
   user: User
-  account: Context
+  account: Context // use Context for internal Account type
path: packages/logs/src/rawLogsEvent.types.ts
user: login: cy-moi
body: I updated the comments.
created_at: 2025-03-07T14:14:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -185,3 +215,23 @@ function makeTracingHeaders(
   })
   return tracingHeaders
 }
+
+function getTraceStateDatadogItems(traceSampled: boolean, getCommonContext: () => CommonContext): string[] {
+  const traceStateDatadogItems: string[] = []
+  traceStateDatadogItems.push(`s:${traceSampled ? '1' : '0'}`)
+  traceStateDatadogItems.push('o:rum')
path: packages/rum-core/src/domain/tracing/tracer.ts
user: login: BenoitZugmeyer
body: **🥜 nitpick:** ‏
```suggestion
  const traceStateDatadogItems: string[] = [
    `s:${traceSampled ? '1' : '0'}`,
    'o:rum'
  ]
```
created_at: 2025-03-07T15:40:28Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,25 @@
+export const encodedContextCache: { [key: string]: string } = {}
path: packages/rum-core/src/domain/tracing/encodedContextCache.ts
user: login: BenoitZugmeyer
body: **💬 suggestion:** ‏use an actual Map, as storing arbitrary strings as keys in an object is a bit unsafe (imagine `setUser({ id: 'hasOwnProperty' })`)
created_at: 2025-03-07T15:42:32Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,38 @@
+import { encodedContextCache, encodeToUtf8Base64, getEncodedContext } from './encodedContextCache'
+
+describe('encodeToUtf8Base64', () => {
+  it('should encode non-ascii to base64 in utf8', () => {
+    // https://developer.mozilla.org/en-US/docs/Web/API/Window/btoa
+    const encoded = encodeToUtf8Base64('a Ā 𐀀 文 🦄')
+    expect(encoded).toBe('YSDEgCDwkICAIOaWhyDwn6aE')
+  })
+})
+
+describe('encodedContextCache', () => {
+  let encodeToUtf8Base64Spy: jasmine.Spy
+  encodedContextCache['text'] = 'encoded'
+  it('should return undefined when input is empty or undefined', () => {
+    encodeToUtf8Base64Spy = jasmine.createSpy()
+    expect(getEncodedContext(undefined, encodeToUtf8Base64Spy)).toBeUndefined()
+    expect(getEncodedContext('', encodeToUtf8Base64Spy)).toBeUndefined()
+    expect(encodeToUtf8Base64Spy).not.toHaveBeenCalled()
path: packages/rum-core/src/domain/tracing/encodedContextCache.spec.ts
user: login: BenoitZugmeyer
body: **💬 suggestion:** ‏instead of spying on `encodeToUtf8Base64`, you could spy on `btoa` or `String.fromCodePoint`

```ts
const btoaSpy = spyOn(window, 'btoa')
...
expect(btoaSpy).not.toHaveBeenCalled()
```

This would make the code a bit simpler as you wouldn't need to provide the function as an argument to `getEncodedContext`
created_at: 2025-03-07T15:47:46Z
author_association: MEMBER
path: packages/rum-core/src/domain/tracing/encodedContextCache.spec.ts
user: login: BenoitZugmeyer
body: **💬 suggestion:** ‏not sure about the file name here... caching seems to be an implementation detail. Maybe rename it to `encodedContext`?
created_at: 2025-03-07T15:49:17Z
author_association: MEMBER

PR comments:
user: login: codecov-commenter
created_at: 2025-03-05T13:42:01Z
body: ## [Codecov](https://app.codecov.io/gh/DataDog/browser-sdk/pull/3395?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DataDog) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 92.76%. Comparing base [(`3e78762`)](https://app.codecov.io/gh/DataDog/browser-sdk/commit/3e78762bf77ca8e53a3788cda08950f3d3b4dd5e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DataDog) to head [(`e75fe68`)](https://app.codecov.io/gh/DataDog/browser-sdk/commit/e75fe680ff91e262401a147b6466ba39374bf176?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DataDog).
> Report is 3 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #3395      +/-   ##
==========================================
+ Coverage   92.73%   92.76%   +0.03%     
==========================================
  Files         300      301       +1     
  Lines        7871     7896      +25     
  Branches     1799     1804       +5     
==========================================
+ Hits         7299     7325      +26     
+ Misses        572      571       -1     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/DataDog/browser-sdk/pull/3395?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DataDog).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DataDog).

<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- 📦 [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: cit-pr-commenter[bot]
created_at: 2025-03-05T13:46:58Z
body: ## Bundles Sizes Evolution

| 📦 Bundle Name | Base Size | Local Size | 𝚫 | 𝚫% | Status |
| --- | --- | --- | --- | --- | --- |
| Rum | 148.24 KiB | 148.73 KiB | 503 B | +0.33% | ✅ |
| Logs | 52.21 KiB | 52.26 KiB | 56 B | +0.10% | ✅ |
| Rum Slim | 106.91 KiB | 107.39 KiB | 495 B | +0.45% | ✅ |
| Worker | 24.50 KiB | 24.50 KiB | 0 B | 0.00% | ✅ |
</details>

<details>
<summary>🚀 CPU Performance</summary>

| Action Name | Base Average Cpu Time (ms) | Local Average Cpu Time (ms) | 𝚫 |
| --- | --- | --- | --- |
| addglobalcontext | 0.002 | 0.002 | 0.000 |
| addaction | 0.051 | 0.038 | -0.013 |
| addtiming | 0.002 | 0.001 | -0.000 |
| adderror | 0.082 | 0.053 | -0.029 |
| startstopsessionreplayrecording | 0.007 | 0.009 | 0.002 |
| startview | 0.387 | 0.360 | -0.027 |
| logmessage | 0.025 | 0.020 | -0.004 |

</details>

<details>
<summary>🧠 Memory Performance</summary>

| Action Name | Base Consumption Memory (bytes) | Local Consumption Memory (bytes) | 𝚫 (bytes) |
| --- | --- | --- | --- |
| addglobalcontext | 28.23 KiB | 28.49 KiB | 271 B |
| addaction | 55.76 KiB | 56.72 KiB | 983 B |
| addtiming | 26.57 KiB | 27.59 KiB | 1.02 KiB |
| adderror | 60.41 KiB | 61.81 KiB | 1.40 KiB |
| startstopsessionreplayrecording | 25.42 KiB | 26.59 KiB | 1.17 KiB |
| startview | 423.02 KiB | 423.73 KiB | 724 B |
| logmessage | 55.45 KiB | 56.73 KiB | 1.28 KiB |

</details>

🔗 [RealWorld](https://datadoghq.dev/browser-sdk-test-playground/realworld-scenario/?prNumber=3395)


author_association: NONE
user: login: cy-moi
created_at: 2025-03-05T15:30:01Z
body: /to-staging
author_association: CONTRIBUTOR
user: login: dd-devflow[bot]
created_at: 2025-03-05T15:30:06Z
body: <!-- "FeedbackWorkflowId":"af21380b-aa72-4182-9475-c58c5e8c25a4_30" -->
[View all feedbacks in Devflow UI.](https://devflow.static-app.us1.prod.dog/logs/af21380b-aa72-4182-9475-c58c5e8c25a4_30)
`2025-03-05 15:30:06 UTC` :information_source: Start processing command `/to-staging`

---
`2025-03-05 15:30:15 UTC` :information_source: Branch Integration: starting soon, median merge time is 13m39s

Commit [91c3e61e1c](https://github.com/DataDog/browser-sdk/commit/91c3e61e1cd3595a4cac5624da84940fb7e95b60) will soon be integrated into [staging-10](https://github.com/DataDog/browser-sdk/tree/staging-10).

---
`2025-03-05 15:30:22 UTC` :warning: Branch Integration: This commit was already integrated

Commit [91c3e61e1c](https://github.com/DataDog/browser-sdk/commit/91c3e61e1cd3595a4cac5624da84940fb7e95b60) had already been merged into [staging-10](https://github.com/DataDog/browser-sdk/tree/staging-10)

If you need to revert this integration, you can use the following command: `/code revert-integration -b staging-10`

author_association: CONTRIBUTOR
user: login: cy-moi
created_at: 2025-03-10T10:07:06Z
body: /to-staging
author_association: CONTRIBUTOR
user: login: dd-devflow[bot]
created_at: 2025-03-10T10:07:09Z
body: <!-- "FeedbackWorkflowId":"3cb19b41-f6e3-403f-aca6-aee150848049_30" -->
[View all feedbacks in Devflow UI.](https://devflow.static-app.us1.prod.dog/logs/3cb19b41-f6e3-403f-aca6-aee150848049_30)
`2025-03-10 10:07:09 UTC` :information_source: Start processing command `/to-staging`

---
`2025-03-10 10:07:19 UTC` :information_source: Branch Integration: starting soon, median merge time is 10m43s

Commit [e75fe680ff](https://github.com/DataDog/browser-sdk/commit/e75fe680ff91e262401a147b6466ba39374bf176) will soon be integrated into [staging-11](https://github.com/DataDog/browser-sdk/tree/staging-11).

---
`2025-03-10 10:20:12 UTC` :information_source: Branch Integration: This commit was successfully integrated

Commit [e75fe680ff](https://github.com/DataDog/browser-sdk/commit/e75fe680ff91e262401a147b6466ba39374bf176) has been merged into [staging-11](https://github.com/DataDog/browser-sdk/tree/staging-11) in merge commit [b577f9b0dd](https://github.com/DataDog/browser-sdk/commit/b577f9b0dd9baad7beaabd1d3f5b3b5e8005b339).

Check out [the triggered pipeline on Gitlab :fox_face:](https://gitlab.ddbuild.io/DataDog/browser-sdk/-/pipelines/58248670)

If you need to revert this integration, you can use the following command: `/code revert-integration -b staging-11`

author_association: CONTRIBUTOR