Skip to content

Commit 519f24c

Browse files
committed
👌 Use CONTEXT_RESOLUTION_HELPERS in E2E tests and fix globalContext ordering
- Export CONTEXT_RESOLUTION_HELPERS from endpoint package index so tests can import it - Replace inline helper copies in E2E setup factories with the canonical CONTEXT_RESOLUTION_HELPERS string (cookie-name regex-escaping, extractor support, DOM password guard) - Fix globalContext test context collision: re-apply test isolation properties via setGlobalContextProperty after init instead of setGlobalContext before init - Fix dynamic cookie test: pre-set cookie via preSDKScript before SDK loads (no second navigation needed) - Consolidate two setup factories into shared createEmbeddedConfigSetup with optional preSDKScript
1 parent 62015fe commit 519f24c

File tree

3 files changed

+86
-166
lines changed

3 files changed

+86
-166
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export { generateBundle, generateCombinedBundle, fetchConfig } from './bundleGenerator.ts'
22
export type { GenerateBundleOptions, CombineBundleOptions, FetchConfigOptions } from './bundleGenerator.ts'
3+
export { CONTEXT_RESOLUTION_HELPERS } from './contextResolutionHelpers.ts'
34

45
// eslint-disable-next-line local-rules/disallow-side-effects -- Node.js build tool, not browser SDK
56
export type { RumRemoteConfiguration, RemoteConfigResult } from '@datadog/browser-remote-config'

‎test/e2e/scenario/rum/embeddedConfig.scenario.ts‎

Lines changed: 73 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,78 @@
1-
import { generateCombinedBundle } from '@datadog/browser-sdk-endpoint'
1+
import { generateCombinedBundle, CONTEXT_RESOLUTION_HELPERS } from '@datadog/browser-sdk-endpoint'
22
import { test, expect } from '@playwright/test'
33
import { createTest, DEFAULT_RUM_CONFIGURATION, html, basePage, createCrossOriginScriptUrls } from '../../lib/framework'
44
import type { SetupOptions, Servers } from '../../lib/framework'
55

6+
export interface ContextItem {
7+
key: string
8+
value: { rcSerializedType: string; value?: string; strategy?: string; name?: string; path?: string; selector?: string; attribute?: string; extractor?: unknown }
9+
}
10+
11+
/**
12+
* Creates a SetupFactory that generates an HTML page mimicking the embedded config bundle
13+
* produced by generateCombinedBundle(), with context resolution for user and globalContext.
14+
* Uses CONTEXT_RESOLUTION_HELPERS verbatim so tests exercise the same code as production.
15+
*
16+
* @param extraConfig - optional user[] / context[] arrays and optional pre-SDK script
17+
* @param extraConfig.user - optional user context items
18+
* @param extraConfig.context - optional global context items
19+
* @param extraConfig.preSDKScript - extra inline JS injected before the SDK script tag (e.g. to pre-set a cookie)
20+
*/
21+
export function createEmbeddedConfigSetup(extraConfig: {
22+
user?: ContextItem[]
23+
context?: ContextItem[]
24+
/** Extra inline JS injected before the SDK script tag (e.g. to pre-set a cookie). */
25+
preSDKScript?: string
26+
}) {
27+
return (options: SetupOptions, servers: Servers): string => {
28+
const { rumScriptUrl } = createCrossOriginScriptUrls(servers, options)
29+
30+
const embeddedConfig = {
31+
...DEFAULT_RUM_CONFIGURATION,
32+
sessionSampleRate: 100,
33+
proxy: servers.intake.origin,
34+
...extraConfig,
35+
}
36+
const configJson = JSON.stringify(embeddedConfig)
37+
const testContextJson = JSON.stringify(options.context)
38+
39+
const header = html`
40+
${extraConfig.preSDKScript ? `<script type="text/javascript">${extraConfig.preSDKScript}</script>` : ''}
41+
<script type="text/javascript" src="${rumScriptUrl}"></script>
42+
<script type="text/javascript">
43+
(function () {
44+
'use strict';
45+
var __DATADOG_REMOTE_CONFIG__ = ${configJson};
46+
var __dd_user = {};
47+
(__DATADOG_REMOTE_CONFIG__.user || []).forEach(function (item) {
48+
__dd_user[item.key] = __dd_resolveContextValue(item.value);
49+
});
50+
var __dd_globalContext = {};
51+
(__DATADOG_REMOTE_CONFIG__.context || []).forEach(function (item) {
52+
__dd_globalContext[item.key] = __dd_resolveContextValue(item.value);
53+
});
54+
var hasUser = Object.keys(__dd_user).length > 0;
55+
var hasGlobalContext = Object.keys(__dd_globalContext).length > 0;
56+
window.DD_RUM.init(Object.assign({}, __DATADOG_REMOTE_CONFIG__, {
57+
user: hasUser ? __dd_user : undefined,
58+
context: undefined,
59+
globalContext: hasGlobalContext ? __dd_globalContext : undefined
60+
}));
61+
// Re-apply test isolation context after init so it is not overwritten by the
62+
// globalContext init option (setContext replaces; we add test properties back).
63+
var __dd_testContext = ${testContextJson};
64+
Object.keys(__dd_testContext).forEach(function(key) {
65+
window.DD_RUM.setGlobalContextProperty(key, __dd_testContext[key]);
66+
});
67+
68+
${CONTEXT_RESOLUTION_HELPERS.trim()}
69+
})();
70+
</script>
71+
`
72+
return basePage({ header })
73+
}
74+
}
75+
676
test.describe('embedded configuration', () => {
777
createTest('should load SDK with embedded config and expose getInitConfiguration')
878
.withRum({
@@ -131,111 +201,18 @@ test.describe('embedded configuration', () => {
131201
})
132202

133203
createTest('should apply static user context from embedded config to view events')
134-
.withSetup(embeddedConfigSetupFactory({ user: [{ key: 'id', value: { rcSerializedType: 'string', value: 'test-user-42' } }] }))
204+
.withSetup(createEmbeddedConfigSetup({ user: [{ key: 'id', value: { rcSerializedType: 'string', value: 'test-user-42' } }] }))
135205
.run(async ({ intakeRegistry, flushEvents }) => {
136206
await flushEvents()
137207
expect(intakeRegistry.rumViewEvents.length).toBeGreaterThanOrEqual(1)
138208
expect(intakeRegistry.rumViewEvents[0].usr?.id).toBe('test-user-42')
139209
})
140210

141211
createTest('should apply static globalContext from embedded config to view events')
142-
.withSetup(embeddedConfigSetupFactory({ context: [{ key: 'plan', value: { rcSerializedType: 'string', value: 'enterprise' } }] }))
212+
.withSetup(createEmbeddedConfigSetup({ context: [{ key: 'plan', value: { rcSerializedType: 'string', value: 'enterprise' } }] }))
143213
.run(async ({ intakeRegistry, flushEvents }) => {
144214
await flushEvents()
145215
expect(intakeRegistry.rumViewEvents.length).toBeGreaterThanOrEqual(1)
146216
expect(intakeRegistry.rumViewEvents[0].context?.plan).toBe('enterprise')
147217
})
148218
})
149-
150-
/**
151-
* Creates a SetupFactory that generates an HTML page mimicking the embedded config bundle
152-
* produced by generateCombinedBundle(), with context resolution for user and globalContext.
153-
*/
154-
function embeddedConfigSetupFactory(extraConfig: {
155-
user?: Array<{ key: string; value: { rcSerializedType: string; value?: string; strategy?: string; name?: string } }>
156-
context?: Array<{ key: string; value: { rcSerializedType: string; value?: string; strategy?: string; name?: string } }>
157-
}) {
158-
return (options: SetupOptions, servers: Servers): string => {
159-
const { rumScriptUrl } = createCrossOriginScriptUrls(servers, options)
160-
161-
const embeddedConfig = {
162-
...DEFAULT_RUM_CONFIGURATION,
163-
sessionSampleRate: 100,
164-
proxy: servers.intake.origin,
165-
...extraConfig,
166-
}
167-
const configJson = JSON.stringify(embeddedConfig)
168-
const testContextJson = JSON.stringify(options.context)
169-
170-
const header = html`
171-
<script type="text/javascript" src="${rumScriptUrl}"></script>
172-
<script type="text/javascript">
173-
(function () {
174-
'use strict';
175-
var __DATADOG_REMOTE_CONFIG__ = ${configJson};
176-
var __dd_user = {};
177-
(__DATADOG_REMOTE_CONFIG__.user || []).forEach(function (item) {
178-
__dd_user[item.key] = __dd_resolveContextValue(item.value);
179-
});
180-
var __dd_globalContext = {};
181-
(__DATADOG_REMOTE_CONFIG__.context || []).forEach(function (item) {
182-
__dd_globalContext[item.key] = __dd_resolveContextValue(item.value);
183-
});
184-
var hasUser = Object.keys(__dd_user).length > 0;
185-
var hasGlobalContext = Object.keys(__dd_globalContext).length > 0;
186-
window.DD_RUM.setGlobalContext(${testContextJson});
187-
window.DD_RUM.init(Object.assign({}, __DATADOG_REMOTE_CONFIG__, {
188-
user: hasUser ? __dd_user : undefined,
189-
context: undefined,
190-
globalContext: hasGlobalContext ? __dd_globalContext : undefined
191-
}));
192-
193-
function __dd_resolveContextValue(value) {
194-
if (!value || typeof value !== 'object') { return value; }
195-
var serializedType = value.rcSerializedType;
196-
if (serializedType === 'string') { return value.value; }
197-
if (serializedType !== 'dynamic') { return undefined; }
198-
var strategy = value.strategy;
199-
var resolved;
200-
if (strategy === 'cookie') {
201-
resolved = __dd_getCookie(value.name);
202-
} else if (strategy === 'js') {
203-
resolved = __dd_resolveJsPath(value.path);
204-
} else if (strategy === 'dom') {
205-
resolved = __dd_resolveDom(value.selector, value.attribute);
206-
} else if (strategy === 'localStorage') {
207-
try { resolved = localStorage.getItem(value.key); } catch(e) { resolved = undefined; }
208-
}
209-
return resolved;
210-
}
211-
212-
function __dd_getCookie(name) {
213-
if (typeof name !== 'string') { return undefined; }
214-
var match = document.cookie.match(new RegExp('(?:^|;\\\\s*)' + name + '=([^;]*)'));
215-
return match ? decodeURIComponent(match[1]) : undefined;
216-
}
217-
218-
function __dd_resolveJsPath(path) {
219-
if (typeof path !== 'string' || path === '') { return undefined; }
220-
var parts = path.split('.');
221-
var obj = window;
222-
for (var i = 0; i < parts.length; i++) {
223-
if (obj == null || !(parts[i] in Object(obj))) { return undefined; }
224-
obj = obj[parts[i]];
225-
}
226-
return obj;
227-
}
228-
229-
function __dd_resolveDom(selector, attribute) {
230-
var el;
231-
try { el = document.querySelector(selector); } catch(e) { return undefined; }
232-
if (!el) { return undefined; }
233-
if (attribute !== undefined) { return el.getAttribute(attribute); }
234-
return el.textContent;
235-
}
236-
})();
237-
</script>
238-
`
239-
return basePage({ header })
240-
}
241-
}

‎test/e2e/scenario/rum/embeddedConfigDynamic.scenario.ts‎

Lines changed: 12 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { test, expect } from '@playwright/test'
22
import { generateCombinedBundle } from '@datadog/browser-sdk-endpoint'
3-
import { createTest, DEFAULT_RUM_CONFIGURATION, html, basePage, createCrossOriginScriptUrls } from '../../lib/framework'
4-
import type { SetupOptions, Servers } from '../../lib/framework'
3+
import { createTest } from '../../lib/framework'
4+
import { createEmbeddedConfigSetup } from './embeddedConfig.scenario'
55

66
test.describe('embedded configuration with dynamic values', () => {
77
test('preserves cookie dynamic value markers in generated bundle', () => {
@@ -158,76 +158,18 @@ test.describe('embedded configuration with dynamic values', () => {
158158
expect(() => new Function(bundle)).not.toThrow()
159159
})
160160

161+
// The cookie is pre-set via a script injected before the SDK loads (preSDKScript),
162+
// so no second navigation is needed and the cookie is available during SDK init.
161163
createTest('should resolve dynamic cookie strategy for user context in embedded config')
162-
.withSetup(embeddedDynamicCookieSetup)
163-
.run(async ({ intakeRegistry, flushEvents, page, baseUrl }) => {
164-
// Set the uid cookie on the current page so it persists for the next navigation
165-
await page.evaluate(() => {
166-
document.cookie = 'uid=cookie-user-99; path=/'
164+
.withSetup(
165+
createEmbeddedConfigSetup({
166+
user: [{ key: 'id', value: { rcSerializedType: 'dynamic', strategy: 'cookie', name: 'uid' } }],
167+
preSDKScript: "document.cookie = 'uid=cookie-user-99; path=/';",
167168
})
168-
// Navigate again so the embedded init code reads the cookie on page load
169-
await page.goto(baseUrl)
169+
)
170+
.run(async ({ intakeRegistry, flushEvents }) => {
170171
await flushEvents()
171-
// At least one view event should have usr.id set to the cookie value.
172-
// (There may be events from the previous page load mixed in, so we check the last one
173-
// which is from the second load where the cookie was available during SDK init.)
174-
const viewEvents = intakeRegistry.rumViewEvents
175-
expect(viewEvents.length).toBeGreaterThanOrEqual(1)
176-
const lastViewEvent = viewEvents[viewEvents.length - 1]
177-
expect(lastViewEvent.usr?.id).toBe('cookie-user-99')
172+
expect(intakeRegistry.rumViewEvents.length).toBeGreaterThanOrEqual(1)
173+
expect(intakeRegistry.rumViewEvents[0].usr?.id).toBe('cookie-user-99')
178174
})
179175
})
180-
181-
/**
182-
* Setup factory for the dynamic cookie user-id test. Serves an HTML page that mimics
183-
* the embedded bundle produced by generateCombinedBundle() with a cookie-based dynamic value.
184-
*/
185-
function embeddedDynamicCookieSetup(options: SetupOptions, servers: Servers): string {
186-
const { rumScriptUrl } = createCrossOriginScriptUrls(servers, options)
187-
188-
const embeddedConfig = {
189-
...DEFAULT_RUM_CONFIGURATION,
190-
sessionSampleRate: 100,
191-
proxy: servers.intake.origin,
192-
user: [{ key: 'id', value: { rcSerializedType: 'dynamic', strategy: 'cookie', name: 'uid' } }],
193-
}
194-
const configJson = JSON.stringify(embeddedConfig)
195-
const testContextJson = JSON.stringify(options.context)
196-
197-
const header = html`
198-
<script type="text/javascript" src="${rumScriptUrl}"></script>
199-
<script type="text/javascript">
200-
(function () {
201-
'use strict';
202-
var __DATADOG_REMOTE_CONFIG__ = ${configJson};
203-
var __dd_user = {};
204-
(__DATADOG_REMOTE_CONFIG__.user || []).forEach(function (item) {
205-
__dd_user[item.key] = __dd_resolveContextValue(item.value);
206-
});
207-
var hasUser = Object.keys(__dd_user).length > 0;
208-
window.DD_RUM.setGlobalContext(${testContextJson});
209-
window.DD_RUM.init(Object.assign({}, __DATADOG_REMOTE_CONFIG__, {
210-
user: hasUser ? __dd_user : undefined,
211-
context: undefined
212-
}));
213-
214-
function __dd_resolveContextValue(value) {
215-
if (!value || typeof value !== 'object') { return value; }
216-
var serializedType = value.rcSerializedType;
217-
if (serializedType === 'string') { return value.value; }
218-
if (serializedType !== 'dynamic') { return undefined; }
219-
var strategy = value.strategy;
220-
if (strategy === 'cookie') { return __dd_getCookie(value.name); }
221-
return undefined;
222-
}
223-
224-
function __dd_getCookie(name) {
225-
if (typeof name !== 'string') { return undefined; }
226-
var match = document.cookie.match(new RegExp('(?:^|;\\\\s*)' + name + '=([^;]*)'));
227-
return match ? decodeURIComponent(match[1]) : undefined;
228-
}
229-
})();
230-
</script>
231-
`
232-
return basePage({ header })
233-
}

0 commit comments

Comments
 (0)