Skip to content

Commit d768238

Browse files
committed
Merge branch 'aymeric/hooks-logs-account' into staging-23
2 parents 12feeec + 5e3d18d commit d768238

19 files changed

Lines changed: 204 additions & 131 deletions
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { RawTelemetryUsage, RawTelemetryUsageFeature } from '../telemetry'
2+
import { addTelemetryUsage } from '../telemetry'
3+
import { monitor } from '../../tools/monitor'
4+
import type { BoundedBuffer } from '../../tools/boundedBuffer'
5+
import type { ContextManager } from './contextManager'
6+
import type { ContextManagerMethod, CustomerContextKey } from './contextConstants'
7+
8+
export function defineContextMethod<MethodName extends ContextManagerMethod, Key extends CustomerContextKey>(
9+
getStrategy: () => Record<Key, ContextManager>,
10+
contextName: Key,
11+
methodName: MethodName,
12+
usage?: RawTelemetryUsageFeature
13+
): ContextManager[MethodName] {
14+
return monitor((...args: any[]) => {
15+
if (usage) {
16+
addTelemetryUsage({ feature: usage } as RawTelemetryUsage)
17+
}
18+
return (getStrategy()[contextName][methodName] as (...args: unknown[]) => unknown)(...args)
19+
}) as ContextManager[MethodName]
20+
}
21+
22+
export function bufferContextCalls<Key extends string, StartResult extends Record<Key, ContextManager>>(
23+
preStartContextManager: ContextManager,
24+
name: Key,
25+
bufferApiCalls: BoundedBuffer<StartResult>
26+
) {
27+
preStartContextManager.changeObservable.subscribe(() => {
28+
const context = preStartContextManager.getContext()
29+
bufferApiCalls.add((startResult) => startResult[name].setContext(context))
30+
})
31+
}

packages/rum-core/src/domain/contexts/accountContext.spec.ts renamed to packages/core/src/domain/contexts/accountContext.spec.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import type { ContextManager, RelativeTime } from '@datadog/browser-core'
2-
import { display, HookNames, removeStorageListeners } from '@datadog/browser-core'
3-
import { registerCleanupTask } from '@datadog/browser-core/test'
4-
import { mockRumConfiguration } from '../../../test'
5-
import type { Hooks } from '../hooks'
6-
import { createHooks } from '../hooks'
1+
import type { Hooks } from '../../../test'
2+
import { createHooks, registerCleanupTask } from '../../../test'
3+
import { mockRumConfiguration } from '../../../../rum-core/test'
4+
import type { ContextManager } from '../context/contextManager'
5+
import { display } from '../../tools/display'
6+
import type { RelativeTime } from '../../tools/utils/timeUtils'
7+
import { HookNames } from '../../tools/abstractHooks'
8+
import { removeStorageListeners } from '../context/storeContextManager'
79
import { startAccountContext } from './accountContext'
810

911
describe('account context', () => {
@@ -13,10 +15,9 @@ describe('account context', () => {
1315

1416
beforeEach(() => {
1517
hooks = createHooks()
16-
1718
displaySpy = spyOn(display, 'warn')
1819

19-
accountContext = startAccountContext(hooks, mockRumConfiguration())
20+
accountContext = startAccountContext(hooks, mockRumConfiguration(), 'some_product_key')
2021
})
2122

2223
it('should warn when the account.id is missing', () => {
@@ -37,12 +38,10 @@ describe('account context', () => {
3738
accountContext.setContext({ id: '123', foo: 'bar' })
3839

3940
const defaultRumEventAttributes = hooks.triggerHook(HookNames.Assemble, {
40-
eventType: 'view',
4141
startTime: 0 as RelativeTime,
4242
})
4343

4444
expect(defaultRumEventAttributes).toEqual({
45-
type: 'view',
4645
account: {
4746
id: '123',
4847
foo: 'bar',
@@ -75,18 +74,26 @@ describe('account context across pages', () => {
7574
})
7675

7776
it('when disabled, should store contexts only in memory', () => {
78-
accountContext = startAccountContext(hooks, mockRumConfiguration({ storeContextsAcrossPages: false }))
77+
accountContext = startAccountContext(
78+
hooks,
79+
mockRumConfiguration({ storeContextsAcrossPages: false }),
80+
'some_product_key'
81+
)
7982
accountContext.setContext({ id: '123' })
8083

8184
expect(accountContext.getContext()).toEqual({ id: '123' })
8285
expect(localStorage.getItem('_dd_c_rum_4')).toBeNull()
8386
})
8487

8588
it('when enabled, should maintain the account in local storage', () => {
86-
accountContext = startAccountContext(hooks, mockRumConfiguration({ storeContextsAcrossPages: true }))
89+
accountContext = startAccountContext(
90+
hooks,
91+
mockRumConfiguration({ storeContextsAcrossPages: true }),
92+
'some_product_key'
93+
)
8794

8895
accountContext.setContext({ id: 'foo', qux: 'qix' })
8996
expect(accountContext.getContext()).toEqual({ id: 'foo', qux: 'qix' })
90-
expect(localStorage.getItem('_dd_c_rum_4')).toBe('{"id":"foo","qux":"qix"}')
97+
expect(localStorage.getItem('_dd_c_some_product_key_4')).toBe('{"id":"foo","qux":"qix"}')
9198
})
9299
})
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { Configuration } from '../configuration'
2+
import { CustomerDataType } from '../context/contextConstants'
3+
import { storeContextManager } from '../context/storeContextManager'
4+
import { HookNames, SKIPPED } from '../../tools/abstractHooks'
5+
import type { AbstractHooks } from '../../tools/abstractHooks'
6+
import type { Account } from '../account.types'
7+
import { isEmptyObject } from '../../tools/utils/objectUtils'
8+
import { createContextManager } from '../context/contextManager'
9+
10+
export function startAccountContext(hooks: AbstractHooks, configuration: Configuration, productKey: string) {
11+
const accountContextManager = buildAccountContextManager()
12+
13+
if (configuration.storeContextsAcrossPages) {
14+
storeContextManager(configuration, accountContextManager, productKey, CustomerDataType.Account)
15+
}
16+
17+
hooks.register(HookNames.Assemble, () => {
18+
const account = accountContextManager.getContext() as Account
19+
20+
if (isEmptyObject(account) || !account.id) {
21+
return SKIPPED
22+
}
23+
24+
return {
25+
account,
26+
}
27+
})
28+
29+
return accountContextManager
30+
}
31+
32+
export function buildAccountContextManager() {
33+
return createContextManager('account', {
34+
propertiesConfig: {
35+
id: { type: 'string', required: true },
36+
name: { type: 'string' },
37+
},
38+
})
39+
}

packages/core/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,9 @@ export { createBoundedBuffer } from './tools/boundedBuffer'
124124
export { catchUserErrors } from './tools/catchUserErrors'
125125
export type { ContextManager } from './domain/context/contextManager'
126126
export { createContextManager } from './domain/context/contextManager'
127+
export { defineContextMethod, bufferContextCalls } from './domain/context/defineContextMethod'
127128
export { storeContextManager, removeStorageListeners } from './domain/context/storeContextManager'
129+
export { startAccountContext, buildAccountContextManager } from './domain/contexts/accountContext'
128130
export { CustomerDataType, CustomerContextKey, ContextManagerMethod } from './domain/context/contextConstants'
129131
export type { ValueHistory, ValueHistoryEntry } from './tools/valueHistory'
130132
export { createValueHistory, CLEAR_OLD_VALUES_INTERVAL } from './tools/valueHistory'

packages/core/src/tools/abstractHooks.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export const SKIPPED = 'SKIPPED'
2626
export type DISCARDED = typeof DISCARDED
2727
export type SKIPPED = typeof SKIPPED
2828

29+
export type AbstractHooks = ReturnType<typeof abstractHooks>
30+
2931
export function abstractHooks<T extends { [K in HookNames]: (...args: any[]) => any }, E>() {
3032
const callbacks: { [K in HookNames]?: Array<T[K]> } = {}
3133

packages/core/test/createHooks.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { HookNames } from '../src/tools/abstractHooks'
2+
import { abstractHooks } from '../src/tools/abstractHooks'
3+
4+
export type Hooks = ReturnType<typeof createHooks>
5+
6+
export const createHooks = abstractHooks<
7+
{
8+
[HookNames.Assemble]: (...args: any[]) => any
9+
},
10+
{ [key: string]: any }
11+
>

packages/core/test/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ export * from './disableJasmineUncaughtExceptionTracking'
2626
export * from './instrumentation'
2727
export * from './wait'
2828
export * from './consoleLog'
29+
export * from './createHooks'

packages/logs/src/boot/logsPublicApi.spec.ts

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ describe('logs entry', () => {
8484
},
8585
context: { foo: 'bar' },
8686
user: {},
87-
account: {},
8887
})
8988
})
9089
})
@@ -321,44 +320,44 @@ describe('logs entry', () => {
321320
})
322321

323322
describe('setAccount', () => {
324-
let logsPublicApi: LogsPublicApi
325323
let displaySpy: jasmine.Spy<() => void>
324+
let logsPublicApi: LogsPublicApi
326325

327326
beforeEach(() => {
328327
displaySpy = spyOn(display, 'error')
329328
logsPublicApi = makeLogsPublicApi(startLogs)
330-
logsPublicApi.init(DEFAULT_INIT_CONFIGURATION)
331329
})
332330

333-
it('should store account in common context', () => {
331+
it('should attach valid objects', () => {
334332
const account = { id: 'foo', name: 'bar', foo: { bar: 'qux' } }
335333
logsPublicApi.setAccount(account)
336334

337-
const getCommonContext = startLogs.calls.mostRecent().args[2]
338-
expect(getCommonContext().account).toEqual({
335+
expect(logsPublicApi.getAccount()).toEqual({
339336
foo: { bar: 'qux' },
340337
id: 'foo',
341338
name: 'bar',
342339
})
340+
expect(displaySpy).not.toHaveBeenCalled()
343341
})
344342

345343
it('should sanitize predefined properties', () => {
346344
const account = { id: false, name: 2 }
347345
logsPublicApi.setAccount(account as any)
348-
const getCommonContext = startLogs.calls.mostRecent().args[2]
349-
expect(getCommonContext().account).toEqual({
346+
347+
expect(logsPublicApi.getAccount()).toEqual({
350348
id: 'false',
351349
name: '2',
352350
})
351+
expect(displaySpy).not.toHaveBeenCalled()
353352
})
354353

355-
it('should clear a previously set account', () => {
356-
const account = { id: 'foo', name: 'bar', foo: 'qux' }
354+
it('should remove the account', () => {
355+
const account = { id: 'foo', name: 'bar' }
357356
logsPublicApi.setAccount(account)
358357
logsPublicApi.clearAccount()
359358

360-
const getCommonContext = startLogs.calls.mostRecent().args[2]
361-
expect(getCommonContext().account).toEqual({})
359+
expect(logsPublicApi.getAccount()).toEqual({})
360+
expect(displaySpy).not.toHaveBeenCalled()
362361
})
363362

364363
it('should reject non object input', () => {
@@ -374,7 +373,6 @@ describe('logs entry', () => {
374373

375374
beforeEach(() => {
376375
logsPublicApi = makeLogsPublicApi(startLogs)
377-
logsPublicApi.init(DEFAULT_INIT_CONFIGURATION)
378376
})
379377

380378
it('should return empty object if no account has been set', () => {
@@ -401,7 +399,6 @@ describe('logs entry', () => {
401399

402400
beforeEach(() => {
403401
logsPublicApi = makeLogsPublicApi(startLogs)
404-
logsPublicApi.init(DEFAULT_INIT_CONFIGURATION)
405402
})
406403

407404
it('should add attribute', () => {
@@ -444,9 +441,7 @@ describe('logs entry', () => {
444441

445442
beforeEach(() => {
446443
logsPublicApi = makeLogsPublicApi(startLogs)
447-
logsPublicApi.init(DEFAULT_INIT_CONFIGURATION)
448444
})
449-
450445
it('should remove property', () => {
451446
const account = { id: 'foo', name: 'bar', email: 'qux', foo: { bar: 'qux' } }
452447

packages/logs/src/boot/logsPublicApi.ts

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import type { Account, Context, TrackingConsent, User, PublicApi } from '@datadog/browser-core'
1+
import type { Account, Context, TrackingConsent, User, PublicApi, ContextManager } from '@datadog/browser-core'
22
import {
3+
ContextManagerMethod,
4+
CustomerContextKey,
35
addTelemetryUsage,
46
CustomerDataType,
57
createContextManager,
@@ -10,6 +12,7 @@ import {
1012
displayAlreadyInitializedError,
1113
deepClone,
1214
createTrackingConsentState,
15+
defineContextMethod,
1316
} from '@datadog/browser-core'
1417
import type { LogsInitConfiguration } from '../domain/configuration'
1518
import type { HandlerType } from '../domain/logger'
@@ -197,6 +200,7 @@ const LOGS_STORAGE_KEY = 'logs'
197200
export interface Strategy {
198201
init: (initConfiguration: LogsInitConfiguration) => void
199202
initConfiguration: LogsInitConfiguration | undefined
203+
accountContext: ContextManager
200204
getInternalContext: StartLogsResult['getInternalContext']
201205
handleLog: StartLogsResult['handleLog']
202206
}
@@ -210,23 +214,16 @@ export function makeLogsPublicApi(startLogsImpl: StartLogs): LogsPublicApi {
210214
email: { type: 'string' },
211215
},
212216
})
213-
const accountContextManager = createContextManager('account', {
214-
propertiesConfig: {
215-
id: { type: 'string', required: true },
216-
name: { type: 'string' },
217-
},
218-
})
219217
const trackingConsentState = createTrackingConsentState()
220218

221219
function getCommonContext() {
222-
return buildCommonContext(globalContextManager, userContextManager, accountContextManager)
220+
return buildCommonContext(globalContextManager, userContextManager)
223221
}
224222

225223
let strategy = createPreStartStrategy(getCommonContext, trackingConsentState, (initConfiguration, configuration) => {
226224
if (initConfiguration.storeContextsAcrossPages) {
227225
storeContextManager(configuration, globalContextManager, LOGS_STORAGE_KEY, CustomerDataType.GlobalContext)
228226
storeContextManager(configuration, userContextManager, LOGS_STORAGE_KEY, CustomerDataType.User)
229-
storeContextManager(configuration, accountContextManager, LOGS_STORAGE_KEY, CustomerDataType.Account)
230227
}
231228

232229
const startLogsResult = startLogsImpl(initConfiguration, configuration, getCommonContext, trackingConsentState)
@@ -235,6 +232,8 @@ export function makeLogsPublicApi(startLogsImpl: StartLogs): LogsPublicApi {
235232
return startLogsResult
236233
})
237234

235+
const getStrategy = () => strategy
236+
238237
const customLoggers: { [name: string]: Logger | undefined } = {}
239238

240239
const mainLogger = new Logger((...params) => strategy.handleLog(...params))
@@ -287,15 +286,27 @@ export function makeLogsPublicApi(startLogsImpl: StartLogs): LogsPublicApi {
287286

288287
clearUser: monitor(userContextManager.clearContext),
289288

290-
setAccount: monitor(accountContextManager.setContext),
289+
setAccount: defineContextMethod(getStrategy, CustomerContextKey.accountContext, ContextManagerMethod.setContext),
291290

292-
getAccount: monitor(accountContextManager.getContext),
291+
getAccount: defineContextMethod(getStrategy, CustomerContextKey.accountContext, ContextManagerMethod.getContext),
293292

294-
setAccountProperty: monitor(accountContextManager.setContextProperty),
293+
setAccountProperty: defineContextMethod(
294+
getStrategy,
295+
CustomerContextKey.accountContext,
296+
ContextManagerMethod.setContextProperty
297+
),
295298

296-
removeAccountProperty: monitor(accountContextManager.removeContextProperty),
299+
removeAccountProperty: defineContextMethod(
300+
getStrategy,
301+
CustomerContextKey.accountContext,
302+
ContextManagerMethod.removeContextProperty
303+
),
297304

298-
clearAccount: monitor(accountContextManager.clearContext),
305+
clearAccount: defineContextMethod(
306+
getStrategy,
307+
CustomerContextKey.accountContext,
308+
ContextManagerMethod.clearContext
309+
),
299310
})
300311
}
301312

packages/logs/src/boot/preStartLogs.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import {
88
initFetchObservable,
99
noop,
1010
timeStampNow,
11+
buildAccountContextManager,
12+
CustomerContextKey,
13+
bufferContextCalls,
1114
} from '@datadog/browser-core'
1215
import {
1316
validateAndBuildLogsConfiguration,
@@ -24,6 +27,11 @@ export function createPreStartStrategy(
2427
doStartLogs: (initConfiguration: LogsInitConfiguration, configuration: LogsConfiguration) => StartLogsResult
2528
): Strategy {
2629
const bufferApiCalls = createBoundedBuffer<StartLogsResult>()
30+
31+
// TODO next major: remove the accountContextManager from preStartStrategy and use an empty context instead
32+
const accountContext = buildAccountContextManager()
33+
bufferContextCalls(accountContext, CustomerContextKey.accountContext, bufferApiCalls)
34+
2735
let cachedInitConfiguration: LogsInitConfiguration | undefined
2836
let cachedConfiguration: LogsConfiguration | undefined
2937
const trackingConsentStateSubscription = trackingConsentState.observable.subscribe(tryStartLogs)
@@ -80,6 +88,8 @@ export function createPreStartStrategy(
8088
return cachedInitConfiguration
8189
},
8290

91+
accountContext,
92+
8393
getInternalContext: noop as () => undefined,
8494

8595
handleLog(message, statusType, handlingStack, context = getCommonContext(), date = timeStampNow()) {

0 commit comments

Comments
 (0)