@amplitude/session-replay-browser 1.47.0-sr-trc-debug-log.3 → 1.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +203 -0
  2. package/lib/cjs/beacon-transport.d.ts +2 -0
  3. package/lib/cjs/beacon-transport.d.ts.map +1 -1
  4. package/lib/cjs/beacon-transport.js +39 -1
  5. package/lib/cjs/beacon-transport.js.map +1 -1
  6. package/lib/cjs/config/joined-config.d.ts +1 -6
  7. package/lib/cjs/config/joined-config.d.ts.map +1 -1
  8. package/lib/cjs/config/joined-config.js +23 -64
  9. package/lib/cjs/config/joined-config.js.map +1 -1
  10. package/lib/cjs/config/local-config.d.ts +4 -6
  11. package/lib/cjs/config/local-config.d.ts.map +1 -1
  12. package/lib/cjs/config/local-config.js +3 -19
  13. package/lib/cjs/config/local-config.js.map +1 -1
  14. package/lib/cjs/config/types.d.ts +74 -19
  15. package/lib/cjs/config/types.d.ts.map +1 -1
  16. package/lib/cjs/config/types.js.map +1 -1
  17. package/lib/cjs/index.d.ts +1 -0
  18. package/lib/cjs/index.d.ts.map +1 -1
  19. package/lib/cjs/index.js.map +1 -1
  20. package/lib/cjs/plugins/url-tracking-plugin.d.ts +0 -13
  21. package/lib/cjs/plugins/url-tracking-plugin.d.ts.map +1 -1
  22. package/lib/cjs/plugins/url-tracking-plugin.js +2 -6
  23. package/lib/cjs/plugins/url-tracking-plugin.js.map +1 -1
  24. package/lib/cjs/session-replay.d.ts +0 -33
  25. package/lib/cjs/session-replay.d.ts.map +1 -1
  26. package/lib/cjs/session-replay.js +40 -335
  27. package/lib/cjs/session-replay.js.map +1 -1
  28. package/lib/cjs/targeting/targeting-manager.d.ts +1 -4
  29. package/lib/cjs/targeting/targeting-manager.d.ts.map +1 -1
  30. package/lib/cjs/targeting/targeting-manager.js +10 -56
  31. package/lib/cjs/targeting/targeting-manager.js.map +1 -1
  32. package/lib/cjs/track-destination.d.ts +5 -1
  33. package/lib/cjs/track-destination.d.ts.map +1 -1
  34. package/lib/cjs/track-destination.js +212 -55
  35. package/lib/cjs/track-destination.js.map +1 -1
  36. package/lib/cjs/version.d.ts +1 -1
  37. package/lib/cjs/version.d.ts.map +1 -1
  38. package/lib/cjs/version.js +1 -1
  39. package/lib/cjs/version.js.map +1 -1
  40. package/lib/cjs/worker/index.js +1 -1
  41. package/lib/esm/beacon-transport.d.ts +2 -0
  42. package/lib/esm/beacon-transport.d.ts.map +1 -1
  43. package/lib/esm/beacon-transport.js +39 -1
  44. package/lib/esm/beacon-transport.js.map +1 -1
  45. package/lib/esm/config/joined-config.d.ts +1 -6
  46. package/lib/esm/config/joined-config.d.ts.map +1 -1
  47. package/lib/esm/config/joined-config.js +23 -64
  48. package/lib/esm/config/joined-config.js.map +1 -1
  49. package/lib/esm/config/local-config.d.ts +4 -6
  50. package/lib/esm/config/local-config.d.ts.map +1 -1
  51. package/lib/esm/config/local-config.js +4 -20
  52. package/lib/esm/config/local-config.js.map +1 -1
  53. package/lib/esm/config/types.d.ts +74 -19
  54. package/lib/esm/config/types.d.ts.map +1 -1
  55. package/lib/esm/config/types.js.map +1 -1
  56. package/lib/esm/index.d.ts +1 -0
  57. package/lib/esm/index.d.ts.map +1 -1
  58. package/lib/esm/index.js.map +1 -1
  59. package/lib/esm/plugins/url-tracking-plugin.d.ts +0 -13
  60. package/lib/esm/plugins/url-tracking-plugin.d.ts.map +1 -1
  61. package/lib/esm/plugins/url-tracking-plugin.js +2 -6
  62. package/lib/esm/plugins/url-tracking-plugin.js.map +1 -1
  63. package/lib/esm/session-replay.d.ts +0 -33
  64. package/lib/esm/session-replay.d.ts.map +1 -1
  65. package/lib/esm/session-replay.js +40 -335
  66. package/lib/esm/session-replay.js.map +1 -1
  67. package/lib/esm/targeting/targeting-manager.d.ts +1 -4
  68. package/lib/esm/targeting/targeting-manager.d.ts.map +1 -1
  69. package/lib/esm/targeting/targeting-manager.js +10 -56
  70. package/lib/esm/targeting/targeting-manager.js.map +1 -1
  71. package/lib/esm/track-destination.d.ts +5 -1
  72. package/lib/esm/track-destination.d.ts.map +1 -1
  73. package/lib/esm/track-destination.js +212 -55
  74. package/lib/esm/track-destination.js.map +1 -1
  75. package/lib/esm/version.d.ts +1 -1
  76. package/lib/esm/version.d.ts.map +1 -1
  77. package/lib/esm/version.js +1 -1
  78. package/lib/esm/version.js.map +1 -1
  79. package/lib/esm/worker/index.js +1 -1
  80. package/lib/scripts/index-min.js +1 -1
  81. package/lib/scripts/index-min.js.gz +0 -0
  82. package/lib/scripts/index-min.js.map +1 -1
  83. package/lib/scripts/observers-min.js +1 -1
  84. package/lib/scripts/observers-min.js.gz +0 -0
  85. package/lib/scripts/session-replay-browser-min.js +1 -1
  86. package/lib/scripts/session-replay-browser-min.js.gz +0 -0
  87. package/lib/scripts/session-replay-browser-min.js.map +1 -1
  88. package/lib/scripts/targeting-min.js +1 -1
  89. package/lib/scripts/targeting-min.js.gz +0 -0
  90. package/lib/scripts/worker-min.js +1 -1
  91. package/lib/scripts/worker-min.js.gz +0 -0
  92. package/package.json +4 -4
  93. package/lib/cjs/diagnostics.d.ts +0 -50
  94. package/lib/cjs/diagnostics.d.ts.map +0 -1
  95. package/lib/cjs/diagnostics.js +0 -67
  96. package/lib/cjs/diagnostics.js.map +0 -1
  97. package/lib/esm/diagnostics.d.ts +0 -50
  98. package/lib/esm/diagnostics.d.ts.map +0 -1
  99. package/lib/esm/diagnostics.js +0 -64
  100. package/lib/esm/diagnostics.js.map +0 -1
package/README.md CHANGED
@@ -106,6 +106,209 @@ sessionReplay.shutdown()
106
106
  |`useWebWorker`|`boolean`|No|`false`|If true, the SDK will compress replay events using a web worker. This offloads compression to a separate thread, improving performance on the main thread.|
107
107
  |`crossOriginIframes.enabled`|`boolean`|No|`false`|Enables cross-origin iframe recording. Must be set to `true` on both the parent page and each child iframe page. See [Cross-Origin Iframe Recording](#cross-origin-iframe-recording).|
108
108
  |`crossOriginIframes.coordinateChildren`|`boolean`|No|`true`|When `true`, the parent SDK automatically sends start/stop signals to child iframes via `postMessage`. Set to `false` to manage child recording lifecycle yourself.|
109
+ |`handleSendEvents`|`function`|No|`undefined`|Custom transport for replay event uploads. Lets you fully own the outbound HTTP call (e.g. to attach a JWT `Authorization` header and route through an authenticated proxy) while the SDK keeps batching, retry and serialization. See [Custom Transport](#custom-transport-authenticated-proxies--jwt).|
110
+ |`handleFetchConfig`|`function`|No|`undefined`|Custom transport for the remote-config fetch. Same contract as `handleSendEvents`, for the config GET. See [Custom Transport](#custom-transport-authenticated-proxies--jwt).|
111
+
112
+ ## Custom Transport (authenticated proxies / JWT)
113
+
114
+ By default the SDK sends replay events and fetches remote config with its own internal `fetch`.
115
+ If your environment requires custom request logic on every outbound call — for example a JWT
116
+ `Authorization` header validated by your own proxy before forwarding to Amplitude — provide the
117
+ `handleSendEvents` and/or `handleFetchConfig` callbacks. The SDK hands each callback a
118
+ fully-formed request and the callback's only job is to execute it and return the `Response`. The
119
+ SDK keeps everything else: URL resolution (including `trackServerUrl` / `configServerUrl`),
120
+ batching, retry/backoff, serialization, compression, and error handling.
121
+
122
+ ```ts
123
+ import * as sessionReplay from '@amplitude/session-replay-browser';
124
+ import type { SendEventsRequest, FetchConfigRequest } from '@amplitude/session-replay-browser';
125
+
126
+ sessionReplay.init(API_KEY, {
127
+ deviceId,
128
+ sessionId,
129
+ // Optional: point at your proxy. The resolved URL is passed into the callbacks.
130
+ trackServerUrl: 'https://my-proxy.example.com/sr-events',
131
+ configServerUrl: 'https://my-proxy.example.com/sr-config',
132
+
133
+ handleSendEvents: async ({ url, method, headers, body, keepalive }: SendEventsRequest) => {
134
+ return fetch(url, {
135
+ method,
136
+ // Spread the SDK headers so you don't drop Content-Encoding etc., then add your auth.
137
+ headers: { ...headers, Authorization: `Bearer ${getJwt()}` },
138
+ body,
139
+ keepalive, // forward this so page-exit batches survive unload
140
+ });
141
+ },
142
+
143
+ handleFetchConfig: async ({ url, method, headers, signal }: FetchConfigRequest) => {
144
+ return fetch(url, {
145
+ method,
146
+ headers: { ...headers, Authorization: `Bearer ${getJwt()}` },
147
+ signal, // honor the SDK's fetch-timeout abort signal
148
+ });
149
+ },
150
+ });
151
+ ```
152
+
153
+ For a cookie-authenticated proxy, omit the `Authorization` header and add `credentials: 'include'`
154
+ instead — the shape is otherwise identical.
155
+
156
+ ### Wiring it up for your integration
157
+
158
+ The two callbacks are the same no matter how you install Session Replay — only *where you pass them
159
+ in* changes. The example above is the **standalone** SDK. If you use the analytics plugin or the
160
+ unified SDK, drop the same `handleSendEvents` / `handleFetchConfig` into your existing init instead:
161
+
162
+ **Plugin — `@amplitude/plugin-session-replay-browser`** (alongside `@amplitude/analytics-browser`):
163
+
164
+ ```ts
165
+ import * as amplitude from '@amplitude/analytics-browser';
166
+ import { sessionReplayPlugin } from '@amplitude/plugin-session-replay-browser';
167
+
168
+ amplitude.add(
169
+ sessionReplayPlugin({
170
+ sampleRate: 1,
171
+ trackServerUrl: 'https://my-proxy.example.com/sr-events',
172
+ configServerUrl: 'https://my-proxy.example.com/sr-config',
173
+ handleSendEvents: async ({ url, method, headers, body, keepalive }) =>
174
+ fetch(url, { method, headers: { ...headers, Authorization: `Bearer ${getJwt()}` }, body, keepalive }),
175
+ handleFetchConfig: async ({ url, method, headers, signal }) =>
176
+ fetch(url, { method, headers: { ...headers, Authorization: `Bearer ${getJwt()}` }, signal }),
177
+ }),
178
+ );
179
+ amplitude.init(API_KEY);
180
+ ```
181
+
182
+ **Unified — `@amplitude/unified`** (the callbacks go under the `sessionReplay` block):
183
+
184
+ ```ts
185
+ import { initAll } from '@amplitude/unified';
186
+
187
+ initAll(API_KEY, {
188
+ sessionReplay: {
189
+ sampleRate: 1,
190
+ trackServerUrl: 'https://my-proxy.example.com/sr-events',
191
+ configServerUrl: 'https://my-proxy.example.com/sr-config',
192
+ handleSendEvents: async ({ url, method, headers, body, keepalive }) =>
193
+ fetch(url, { method, headers: { ...headers, Authorization: `Bearer ${getJwt()}` }, body, keepalive }),
194
+ handleFetchConfig: async ({ url, method, headers, signal }) =>
195
+ fetch(url, { method, headers: { ...headers, Authorization: `Bearer ${getJwt()}` }, signal }),
196
+ },
197
+ });
198
+ ```
199
+
200
+ Everything else in this section — the contract, the proxy-side requirements, web-worker support —
201
+ applies identically across all three.
202
+
203
+ ### Contract
204
+
205
+ What the SDK guarantees to your callback:
206
+
207
+ - It passes a fully-formed request (resolved URL, default headers, serialized body). Your only job
208
+ is to execute it and return a `Response`.
209
+ - It calls the callback **once per logical request attempt**. Retry/backoff is handled by the SDK
210
+ *around* the callback, so do not implement your own retry.
211
+ - Non-2xx responses and thrown errors / rejected promises are surfaced to the SDK's existing retry
212
+ and logging logic, exactly as the built-in path treats them.
213
+
214
+ What the SDK requires from your callback:
215
+
216
+ - It MUST return a `Response` (or a Response-like object with `ok`, `status`, `text()`).
217
+ - It MUST NOT change the semantics of the SDK-supplied body. Forward it unchanged — when transport
218
+ compression is active, `body` is a gzipped `Uint8Array` and `headers` already includes
219
+ `Content-Encoding: gzip`, so keep them together (don't drop that header while forwarding the
220
+ compressed body, or the payload won't decode server-side).
221
+
222
+ ### Notes
223
+
224
+ - **You own auth, not Amplitude.** Amplitude never validates your JWT; authentication happens in
225
+ your proxy, which forwards the (re-authenticated) request to Amplitude.
226
+ - **Web Worker mode is supported.** When `useWebWorker: true`, the SDK keeps compressing off the
227
+ main thread and delegates only the network call back to the main thread to run your callback.
228
+ - **Page exit.** On unload the SDK normally uses `navigator.sendBeacon`, which can't carry custom
229
+ headers. When `handleSendEvents` is set, the SDK instead routes the final batch through your
230
+ callback with `keepalive: true`, so the exit batch is still authenticated.
231
+ - **Covers all outbound traffic.** When `handleSendEvents` is set it is used for every outbound
232
+ Session Replay request: replay event uploads, the page-exit beacon, and interaction/scroll
233
+ beacons (these otherwise send via `navigator.sendBeacon` and would not carry your auth). So no
234
+ Session Replay request leaves the page unauthenticated.
235
+
236
+ ### Implementing the proxy side
237
+
238
+ The callbacks above are only half the story — they get the request out of the browser with your
239
+ auth attached. The other half is **your proxy**, which has to check that auth and then forward the
240
+ request on to Amplitude. You build and run this proxy; Amplitude doesn't provide one, and you don't
241
+ need us to stand up any new endpoint. Here's everything you need to get it right.
242
+
243
+ #### Where to forward to
244
+
245
+ Your proxy forwards to Amplitude's existing public Session Replay endpoints — the same ones the SDK
246
+ would have hit directly. Pick the row that matches your project's data region:
247
+
248
+ | What | US | EU |
249
+ | ---- | -- | -- |
250
+ | Replay events (the `POST` from `handleSendEvents`) | `https://api-sr.amplitude.com/sessions/v2/track` | `https://api-sr.eu.amplitude.com/sessions/v2/track` |
251
+ | Remote config (the `GET` from `handleFetchConfig`) | `https://sr-client-cfg.amplitude.com/config` | `https://sr-client-cfg.eu.amplitude.com/config` |
252
+
253
+ So the round trip is just: **browser → your proxy → Amplitude → your proxy → browser.** Your proxy
254
+ sits in the middle, checks the JWT, and relays everything else through.
255
+
256
+ #### What your proxy needs to do
257
+
258
+ 1. **Validate your auth, then strip it.** Read the JWT (or whatever you attached in the callback),
259
+ verify it however your security team wants, and reject the request if it's bad. Don't pass your
260
+ JWT on to Amplitude — Amplitude doesn't know what it is and doesn't need it.
261
+
262
+ 2. **Keep the path and query string exactly as-is.** The SDK builds URLs like
263
+ `…/sessions/v2/track?device_id=…&session_id=…&type=replay` and
264
+ `…/config/<apiKey>?config_group=browser`. Those query params matter — forward them unchanged.
265
+ Don't drop or rewrite them.
266
+
267
+ 3. **Forward the body untouched, and keep `Content-Encoding`.** The replay body is usually gzipped,
268
+ and the request carries a `Content-Encoding: gzip` header to say so. Pass the bytes through
269
+ exactly as received and keep that header on. If you decompress, re-compress, or drop the header,
270
+ Amplitude won't be able to read the payload.
271
+
272
+ 4. **Make sure Amplitude can still see your API key.** This is the one that trips people up — see
273
+ below.
274
+
275
+ #### The API-key gotcha (read this one)
276
+
277
+ Amplitude identifies your project from your **Amplitude API key**, which the SDK puts in the
278
+ `Authorization: Bearer <amplitudeApiKey>` header. If your callback does the obvious thing and
279
+ *overwrites* that header with your JWT, the API key is gone by the time the request leaves the
280
+ browser — and Amplitude will reject the forwarded request because it can't tell which project it
281
+ belongs to.
282
+
283
+ Two ways to handle it. **We recommend the second one** — it's simpler and harder to get wrong:
284
+
285
+ - **Option A — your proxy puts the API key back.** Your callback overwrites `Authorization` with
286
+ the JWT; your proxy validates the JWT and then swaps `Authorization` back to
287
+ `Bearer <amplitudeApiKey>` before forwarding. Works, but your proxy now has to know your Amplitude
288
+ API key.
289
+
290
+ - **Option B (recommended) — send the JWT in a different header.** Leave the SDK's `Authorization`
291
+ header alone and put your JWT somewhere else, e.g. `X-Customer-Auth`. Your proxy checks that
292
+ header, strips it, and forwards everything else (including the untouched `Authorization`) straight
293
+ through. Your proxy never has to know or handle the Amplitude API key.
294
+
295
+ ```ts
296
+ handleSendEvents: async ({ url, method, headers, body, keepalive }) =>
297
+ fetch(url, {
298
+ method,
299
+ // Don't overwrite Authorization — add the JWT under your own header instead.
300
+ headers: { ...headers, 'X-Customer-Auth': await getJwt() },
301
+ body,
302
+ keepalive,
303
+ }),
304
+ ```
305
+
306
+ #### Quick sanity check
307
+
308
+ Once your proxy is up, do a quick end-to-end test: load a page with the SDK pointed at your proxy,
309
+ interact a bit, and confirm (a) your proxy logs show the JWT arriving and getting validated, and
310
+ (b) replays actually show up in Amplitude. If replays don't land, it's almost always one of the four
311
+ points above — most often the API-key header (gotcha above) or a dropped `Content-Encoding`.
109
312
 
110
313
  ## Cross-Origin Iframe Recording
111
314
 
@@ -22,6 +22,8 @@ export declare class BeaconTransport<T> {
22
22
  private readonly basePageUrl;
23
23
  private readonly context;
24
24
  private readonly apiKey;
25
+ private readonly handleSendEvents?;
26
+ private readonly loggerProvider?;
25
27
  constructor(context: Omit<SessionReplayDestinationSessionMetadata, 'deviceId'>, config: SessionReplayJoinedConfig);
26
28
  send(deviceId: string, payload: T): void;
27
29
  }
@@ -1 +1 @@
1
- {"version":3,"file":"beacon-transport.d.ts","sourceRoot":"","sources":["../../src/beacon-transport.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAC3D,OAAO,EAAE,uCAAuC,EAAE,MAAM,0BAA0B,CAAC;AAKnF;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,eAAe,CAAC,CAAC;IAC5B,OAAO,CAAC,UAAU,CAAkB;IACpC,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4D;IACpF,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEpB,OAAO,EAAE,IAAI,CAAC,uCAAuC,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,yBAAyB;IA8BjH,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;CAelC"}
1
+ {"version":3,"file":"beacon-transport.d.ts","sourceRoot":"","sources":["../../src/beacon-transport.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,yBAAyB,EAAkC,MAAM,gBAAgB,CAAC;AAC3F,OAAO,EAAE,uCAAuC,EAAE,MAAM,0BAA0B,CAAC;AAKnF;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,eAAe,CAAC,CAAC;IAC5B,OAAO,CAAC,UAAU,CAAkB;IACpC,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4D;IACpF,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAGhC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC,CAAiC;IACnE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAU;gBAE9B,OAAO,EAAE,IAAI,CAAC,uCAAuC,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,yBAAyB;IAgCjH,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;CAiDlC"}
@@ -48,9 +48,47 @@ var BeaconTransport = /** @class */ (function () {
48
48
  this.basePageUrl = (0, helpers_1.getServerUrl)(config.serverZone, config.trackServerUrl);
49
49
  this.apiKey = config.apiKey;
50
50
  this.context = context;
51
+ this.handleSendEvents = config.handleSendEvents;
52
+ this.loggerProvider = config.loggerProvider;
51
53
  }
52
54
  BeaconTransport.prototype.send = function (deviceId, payload) {
53
- var _a = this.context, sessionId = _a.sessionId, type = _a.type;
55
+ var _this = this;
56
+ var _a;
57
+ var _b = this.context, sessionId = _b.sessionId, type = _b.type;
58
+ // Custom-transport path: navigator.sendBeacon/XHR cannot attach custom headers, so an
59
+ // interaction beacon would otherwise leave unauthenticated and be rejected by an
60
+ // authenticating proxy. When handleSendEvents is set, route the beacon through it instead —
61
+ // a keepalive fetch (survives page unload) that carries the customer's auth. Mirrors the
62
+ // replay page-exit path: api_key goes in the Authorization header, not the URL.
63
+ if (this.handleSendEvents) {
64
+ var params = new URLSearchParams({
65
+ device_id: deviceId,
66
+ session_id: String(sessionId),
67
+ type: String(type),
68
+ });
69
+ var url = "".concat(this.basePageUrl, "?").concat(params.toString());
70
+ var headers = {
71
+ 'Content-Type': 'application/json',
72
+ Accept: '*/*',
73
+ Authorization: "Bearer ".concat(this.apiKey),
74
+ };
75
+ try {
76
+ void this.handleSendEvents({
77
+ url: url,
78
+ method: 'POST',
79
+ headers: headers,
80
+ body: JSON.stringify(payload),
81
+ keepalive: true,
82
+ }).catch(function (e) {
83
+ var _a;
84
+ (_a = _this.loggerProvider) === null || _a === void 0 ? void 0 : _a.warn('Custom transport failed to send session replay interaction beacon:', e);
85
+ });
86
+ }
87
+ catch (e) {
88
+ (_a = this.loggerProvider) === null || _a === void 0 ? void 0 : _a.warn('Custom transport threw while sending session replay interaction beacon:', e);
89
+ }
90
+ return;
91
+ }
54
92
  var urlParams = new URLSearchParams({
55
93
  device_id: deviceId,
56
94
  session_id: String(sessionId),
@@ -1 +1 @@
1
- {"version":3,"file":"beacon-transport.js","sourceRoot":"","sources":["../../src/beacon-transport.ts"],"names":[],"mappings":";;;AAAA,4DAA2D;AAG3D,qCAAyC;AAIzC;;;;;;;;;;;;;;;GAeG;AACH;IAOE,yBAAY,OAAkE,EAAE,MAAiC;QAC/G,IAAM,WAAW,GAAG,IAAA,+BAAc,GAAE,CAAC;QACrC,IAAI,WAAW,IAAI,WAAW,CAAC,SAAS,IAAI,OAAO,WAAW,CAAC,SAAS,CAAC,UAAU,KAAK,UAAU,EAAE;YAClG,IAAI,CAAC,UAAU,GAAG,UAAC,OAAO,EAAE,OAAO;gBACjC,IAAI;oBACF,IAAI,WAAW,CAAC,SAAS,CAAC,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,EAAE;wBACtE,OAAO,IAAI,CAAC;qBACb;iBACF;gBAAC,OAAO,CAAC,EAAE;oBACV,4EAA4E;iBAC7E;gBACD,OAAO,KAAK,CAAC;YACf,CAAC,CAAC;SACH;aAAM;YACL,IAAI,CAAC,UAAU,GAAG,cAAM,OAAA,KAAK,EAAL,CAAK,CAAC;SAC/B;QAED,IAAI,CAAC,OAAO,GAAG,UAAC,OAAO,EAAE,OAAO;YAC9B,IAAM,GAAG,GAAG,IAAI,cAAc,EAAE,CAAC;YACjC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;YAChC,GAAG,CAAC,gBAAgB,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;YACtC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC;YAClC,OAAO,IAAI,CAAC;QACd,CAAC,CAAC;QAEF,IAAI,CAAC,WAAW,GAAG,IAAA,sBAAY,EAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,cAAc,CAAC,CAAC;QAC1E,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;QAC5B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED,8BAAI,GAAJ,UAAK,QAAgB,EAAE,OAAU;QACzB,IAAA,KAAsB,IAAI,CAAC,OAAO,EAAhC,SAAS,eAAA,EAAE,IAAI,UAAiB,CAAC;QACzC,IAAM,SAAS,GAAG,IAAI,eAAe,CAAC;YACpC,SAAS,EAAE,QAAQ;YACnB,UAAU,EAAE,MAAM,CAAC,SAAS,CAAC;YAC7B,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC;YAClB,OAAO,EAAE,IAAI,CAAC,MAAM;SACrB,CAAC,CAAC;QAEH,IAAM,OAAO,GAAG,UAAG,IAAI,CAAC,WAAW,cAAI,SAAS,CAAC,QAAQ,EAAE,CAAE,CAAC;QAE9D,kGAAkG;QAClG,+DAA+D;QAC/D,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACtE,CAAC;IACH,sBAAC;AAAD,CAAC,AApDD,IAoDC;AApDY,0CAAe","sourcesContent":["import { getGlobalScope } from '@amplitude/analytics-core';\nimport { SessionReplayJoinedConfig } from './config/types';\nimport { SessionReplayDestinationSessionMetadata } from './typings/session-replay';\nimport { getServerUrl } from './helpers';\n\ntype BeaconSendFn<T> = (pageUrl: string, payload: T) => boolean;\n\n/**\n * For very small payloads it's preferable to use the [Beacon API](https://developer.mozilla.org/en-US/docs/Web/API/Beacon_API).\n * While it doesn't provide 100% guarantees on sends, it greatly helps with overall reliability and page load performance. As\n * the Beacon API has a potential to fail due to size constraints we want to fall back to XHR if need be. This is mostly to\n * be used with 'pagehide' or 'beforeunload' events.\n *\n * Note there are only 3 CORS safelisted Content-Types you can send:\n *\n * - application/x-www-form-urlencoded\n * - multipart/form-data\n * - text/plain\n *\n * If we do not send one of these, some browsers like Chrome may not send this at all. Also we incur the overhead of a preflight\n * request. In our case we will add no additional content-type header. If you are trying to ping a server that requires this\n * header, you may want to use the regular fetch API or a different mechanism.\n */\nexport class BeaconTransport<T> {\n private sendBeacon: BeaconSendFn<T>;\n private sendXhr: BeaconSendFn<T>;\n private readonly basePageUrl: string;\n private readonly context: Omit<SessionReplayDestinationSessionMetadata, 'deviceId'>;\n private readonly apiKey: string;\n\n constructor(context: Omit<SessionReplayDestinationSessionMetadata, 'deviceId'>, config: SessionReplayJoinedConfig) {\n const globalScope = getGlobalScope();\n if (globalScope && globalScope.navigator && typeof globalScope.navigator.sendBeacon === 'function') {\n this.sendBeacon = (pageUrl, payload) => {\n try {\n if (globalScope.navigator.sendBeacon(pageUrl, JSON.stringify(payload))) {\n return true;\n }\n } catch (e) {\n // not logging error, since it would be hard to view and just adds overhead.\n }\n return false;\n };\n } else {\n this.sendBeacon = () => false;\n }\n\n this.sendXhr = (pageUrl, payload) => {\n const xhr = new XMLHttpRequest();\n xhr.open('POST', pageUrl, true);\n xhr.setRequestHeader('Accept', '*/*');\n xhr.send(JSON.stringify(payload));\n return true;\n };\n\n this.basePageUrl = getServerUrl(config.serverZone, config.trackServerUrl);\n this.apiKey = config.apiKey;\n this.context = context;\n }\n\n send(deviceId: string, payload: T) {\n const { sessionId, type } = this.context;\n const urlParams = new URLSearchParams({\n device_id: deviceId,\n session_id: String(sessionId),\n type: String(type),\n api_key: this.apiKey,\n });\n\n const pageUrl = `${this.basePageUrl}?${urlParams.toString()}`;\n\n // ideally send using the beacon API, but there is a chance it may fail, possibly due to a payload\n // size limit. in this case, try best effort to send using xhr.\n this.sendBeacon(pageUrl, payload) || this.sendXhr(pageUrl, payload);\n }\n}\n"]}
1
+ {"version":3,"file":"beacon-transport.js","sourceRoot":"","sources":["../../src/beacon-transport.ts"],"names":[],"mappings":"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","sourcesContent":["import { getGlobalScope, ILogger } from '@amplitude/analytics-core';\nimport { SessionReplayJoinedConfig, SessionReplaySendEventsHandler } from './config/types';\nimport { SessionReplayDestinationSessionMetadata } from './typings/session-replay';\nimport { getServerUrl } from './helpers';\n\ntype BeaconSendFn<T> = (pageUrl: string, payload: T) => boolean;\n\n/**\n * For very small payloads it's preferable to use the [Beacon API](https://developer.mozilla.org/en-US/docs/Web/API/Beacon_API).\n * While it doesn't provide 100% guarantees on sends, it greatly helps with overall reliability and page load performance. As\n * the Beacon API has a potential to fail due to size constraints we want to fall back to XHR if need be. This is mostly to\n * be used with 'pagehide' or 'beforeunload' events.\n *\n * Note there are only 3 CORS safelisted Content-Types you can send:\n *\n * - application/x-www-form-urlencoded\n * - multipart/form-data\n * - text/plain\n *\n * If we do not send one of these, some browsers like Chrome may not send this at all. Also we incur the overhead of a preflight\n * request. In our case we will add no additional content-type header. If you are trying to ping a server that requires this\n * header, you may want to use the regular fetch API or a different mechanism.\n */\nexport class BeaconTransport<T> {\n private sendBeacon: BeaconSendFn<T>;\n private sendXhr: BeaconSendFn<T>;\n private readonly basePageUrl: string;\n private readonly context: Omit<SessionReplayDestinationSessionMetadata, 'deviceId'>;\n private readonly apiKey: string;\n // Optional custom transport. When set, interaction beacons go through it (a keepalive fetch\n // that can carry custom auth) instead of navigator.sendBeacon/XHR, which cannot set headers.\n private readonly handleSendEvents?: SessionReplaySendEventsHandler;\n private readonly loggerProvider?: ILogger;\n\n constructor(context: Omit<SessionReplayDestinationSessionMetadata, 'deviceId'>, config: SessionReplayJoinedConfig) {\n const globalScope = getGlobalScope();\n if (globalScope && globalScope.navigator && typeof globalScope.navigator.sendBeacon === 'function') {\n this.sendBeacon = (pageUrl, payload) => {\n try {\n if (globalScope.navigator.sendBeacon(pageUrl, JSON.stringify(payload))) {\n return true;\n }\n } catch (e) {\n // not logging error, since it would be hard to view and just adds overhead.\n }\n return false;\n };\n } else {\n this.sendBeacon = () => false;\n }\n\n this.sendXhr = (pageUrl, payload) => {\n const xhr = new XMLHttpRequest();\n xhr.open('POST', pageUrl, true);\n xhr.setRequestHeader('Accept', '*/*');\n xhr.send(JSON.stringify(payload));\n return true;\n };\n\n this.basePageUrl = getServerUrl(config.serverZone, config.trackServerUrl);\n this.apiKey = config.apiKey;\n this.context = context;\n this.handleSendEvents = config.handleSendEvents;\n this.loggerProvider = config.loggerProvider;\n }\n\n send(deviceId: string, payload: T) {\n const { sessionId, type } = this.context;\n\n // Custom-transport path: navigator.sendBeacon/XHR cannot attach custom headers, so an\n // interaction beacon would otherwise leave unauthenticated and be rejected by an\n // authenticating proxy. When handleSendEvents is set, route the beacon through it instead —\n // a keepalive fetch (survives page unload) that carries the customer's auth. Mirrors the\n // replay page-exit path: api_key goes in the Authorization header, not the URL.\n if (this.handleSendEvents) {\n const params = new URLSearchParams({\n device_id: deviceId,\n session_id: String(sessionId),\n type: String(type),\n });\n const url = `${this.basePageUrl}?${params.toString()}`;\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n Accept: '*/*',\n Authorization: `Bearer ${this.apiKey}`,\n };\n try {\n void this.handleSendEvents({\n url,\n method: 'POST',\n headers,\n body: JSON.stringify(payload),\n keepalive: true,\n }).catch((e) => {\n this.loggerProvider?.warn('Custom transport failed to send session replay interaction beacon:', e);\n });\n } catch (e) {\n this.loggerProvider?.warn('Custom transport threw while sending session replay interaction beacon:', e);\n }\n return;\n }\n\n const urlParams = new URLSearchParams({\n device_id: deviceId,\n session_id: String(sessionId),\n type: String(type),\n api_key: this.apiKey,\n });\n\n const pageUrl = `${this.basePageUrl}?${urlParams.toString()}`;\n\n // ideally send using the beacon API, but there is a chance it may fail, possibly due to a payload\n // size limit. in this case, try best effort to send using xhr.\n this.sendBeacon(pageUrl, payload) || this.sendXhr(pageUrl, payload);\n }\n}\n"]}
@@ -5,12 +5,7 @@ export declare const removeInvalidSelectorsFromPrivacyConfig: (privacyConfig: Pr
5
5
  export declare class SessionReplayJoinedConfigGenerator {
6
6
  private readonly localConfig;
7
7
  private readonly remoteConfigClient;
8
- private readonly sessionId?;
9
- private readonly deviceId?;
10
- constructor(remoteConfigClient: IRemoteConfigClient, localConfig: ISessionReplayLocalConfig, identity?: {
11
- sessionId?: string | number;
12
- deviceId?: string;
13
- });
8
+ constructor(remoteConfigClient: IRemoteConfigClient, localConfig: ISessionReplayLocalConfig);
14
9
  generateJoinedConfig(): Promise<SessionReplayConfigs>;
15
10
  /**
16
11
  * Defensive bounds for the remote-supplied min_session_duration_ms. A misconfigured
@@ -1 +1 @@
1
- {"version":3,"file":"joined-config.d.ts","sourceRoot":"","sources":["../../../src/config/joined-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,mBAAmB,EAA4C,MAAM,2BAA2B,CAAC;AAGnH,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAEjE,OAAO,EACL,wBAAwB,IAAI,yBAAyB,EACrD,aAAa,EACb,oBAAoB,EAGrB,MAAM,SAAS,CAAC;AAajB,eAAO,MAAM,uCAAuC,kBAAmB,aAAa,kBAAkB,OAAO,kBA0B5G,CAAC;AACF,qBAAa,kCAAkC;IAC7C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA4B;IACxD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAsB;IAEzD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAkB;IAC7C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAS;gBAGjC,kBAAkB,EAAE,mBAAmB,EACvC,WAAW,EAAE,yBAAyB,EACtC,QAAQ,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE;IAQzD,oBAAoB,IAAI,OAAO,CAAC,oBAAoB,CAAC;IAwO3D;;;;;;OAMG;IACH,OAAO,CAAC,4BAA4B;CAmBrC;AAED;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,QAAS,CAAC;AAElD,eAAO,MAAM,wCAAwC,WAAkB,MAAM,WAAW,oBAAoB,gDAc3G,CAAC"}
1
+ {"version":3,"file":"joined-config.d.ts","sourceRoot":"","sources":["../../../src/config/joined-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,mBAAmB,EAA4C,MAAM,2BAA2B,CAAC;AAEnH,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAEjE,OAAO,EACL,wBAAwB,IAAI,yBAAyB,EACrD,aAAa,EACb,oBAAoB,EAGrB,MAAM,SAAS,CAAC;AAajB,eAAO,MAAM,uCAAuC,kBAAmB,aAAa,kBAAkB,OAAO,kBA0B5G,CAAC;AACF,qBAAa,kCAAkC;IAC7C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA4B;IACxD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAsB;gBAE7C,kBAAkB,EAAE,mBAAmB,EAAE,WAAW,EAAE,yBAAyB;IAKrF,oBAAoB,IAAI,OAAO,CAAC,oBAAoB,CAAC;IA+L3D;;;;;;OAMG;IACH,OAAO,CAAC,4BAA4B;CAmBrC;AAED;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,QAAS,CAAC;AAElD,eAAO,MAAM,wCAAwC,WAAkB,MAAM,WAAW,oBAAoB,gDAc3G,CAAC"}
@@ -4,7 +4,6 @@ exports.createSessionReplayJoinedConfigGenerator = exports.MAX_MIN_SESSION_DURAT
4
4
  var tslib_1 = require("tslib");
5
5
  var analytics_core_1 = require("@amplitude/analytics-core");
6
6
  var helpers_1 = require("../helpers");
7
- var diagnostics_1 = require("../diagnostics");
8
7
  var local_config_1 = require("./local-config");
9
8
  // Budget for waiting on the remote config response before falling back to the local cache.
10
9
  // The inner fetch in analytics-core uses a 1000ms per-attempt AbortController timeout with
@@ -46,20 +45,18 @@ var removeInvalidSelectorsFromPrivacyConfig = function (privacyConfig, loggerPro
46
45
  };
47
46
  exports.removeInvalidSelectorsFromPrivacyConfig = removeInvalidSelectorsFromPrivacyConfig;
48
47
  var SessionReplayJoinedConfigGenerator = /** @class */ (function () {
49
- function SessionReplayJoinedConfigGenerator(remoteConfigClient, localConfig, identity) {
48
+ function SessionReplayJoinedConfigGenerator(remoteConfigClient, localConfig) {
50
49
  this.localConfig = localConfig;
51
50
  this.remoteConfigClient = remoteConfigClient;
52
- this.sessionId = identity === null || identity === void 0 ? void 0 : identity.sessionId;
53
- this.deviceId = identity === null || identity === void 0 ? void 0 : identity.deviceId;
54
51
  }
55
52
  SessionReplayJoinedConfigGenerator.prototype.generateJoinedConfig = function () {
56
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
53
+ var _a, _b, _c, _d, _e, _f, _g;
57
54
  return tslib_1.__awaiter(this, void 0, void 0, function () {
58
- var config, sessionReplayRemoteConfig, error_1, samplingConfig, remotePrivacyConfig, targetingConfig, localPrivacyConfig, joinedPrivacyConfig, privacyConfigSelectorMap, selectorMap, _l, _m, _o, selector, selectorType;
59
- var e_1, _p;
55
+ var config, sessionReplayRemoteConfig, error_1, samplingConfig, remotePrivacyConfig, targetingConfig, localPrivacyConfig, joinedPrivacyConfig, privacyConfigSelectorMap, selectorMap, _h, _j, _k, selector, selectorType;
56
+ var e_1, _l;
60
57
  var _this = this;
61
- return tslib_1.__generator(this, function (_q) {
62
- switch (_q.label) {
58
+ return tslib_1.__generator(this, function (_m) {
59
+ switch (_m.label) {
63
60
  case 0:
64
61
  config = tslib_1.__assign({}, this.localConfig);
65
62
  // Special case here as optOut is implemented via getter/setter
@@ -67,16 +64,16 @@ var SessionReplayJoinedConfigGenerator = /** @class */ (function () {
67
64
  // We always want captureEnabled to be true, unless there's an override
68
65
  // in the remote config.
69
66
  config.captureEnabled = true;
70
- _q.label = 1;
67
+ _m.label = 1;
71
68
  case 1:
72
- _q.trys.push([1, 3, , 4]);
69
+ _m.trys.push([1, 3, , 4]);
73
70
  // Subscribe with a timeout so the SDK prefers the remote response and only falls back
74
71
  // to cache after the budget elapses. 'all' mode would race a synchronous cache read
75
72
  // against the network and resolve on whichever fires first — cache always wins, so a
76
73
  // stale cache silently overrides the live config (SR-4234).
77
74
  return [4 /*yield*/, new Promise(function (resolve, reject) {
78
75
  _this.remoteConfigClient.subscribe('configs.sessionReplay', { timeout: REMOTE_CONFIG_TIMEOUT_MS }, function (remoteConfig, source) {
79
- var _a, _b;
76
+ var _a;
80
77
  _this.localConfig.loggerProvider.debug("Session Replay remote configuration received from ".concat(source, ":"), JSON.stringify(remoteConfig, null, 2));
81
78
  if (!remoteConfig) {
82
79
  reject(new Error('No remote config received'));
@@ -87,10 +84,6 @@ var SessionReplayJoinedConfigGenerator = /** @class */ (function () {
87
84
  var samplingConfig = namespaceConfig.sr_sampling_config;
88
85
  var privacyConfig = namespaceConfig.sr_privacy_config;
89
86
  var targetingConfig = namespaceConfig.sr_targeting_config;
90
- // Captured for the sr.trc.config.received diagnostics event below (remote vs cache is
91
- // the crux of SR-4234 stale-cache reports).
92
- var samplingForLog = samplingConfig;
93
- var targetingSegments = targetingConfig === null || targetingConfig === void 0 ? void 0 : targetingConfig.segments;
94
87
  var ugcFilterRules = (_a = config.interactionConfig) === null || _a === void 0 ? void 0 : _a.ugcFilterRules;
95
88
  // This is intentionally forced to only be set through the remote config.
96
89
  config.interactionConfig = namespaceConfig.sr_interaction_config;
@@ -99,33 +92,6 @@ var SessionReplayJoinedConfigGenerator = /** @class */ (function () {
99
92
  }
100
93
  // This is intentionally forced to only be set through the remote config.
101
94
  config.loggingConfig = namespaceConfig.sr_logging_config;
102
- // Team-visible diagnostics: which SOURCE the config came from (remote vs cache — the
103
- // crux of SR-4234) and whether targeting was present. Counters aggregate across the
104
- // customer's sessions; the event carries the full context (source, sample rate,
105
- // segment count) as queryable log fields. No-op when diagnostics isn't configured.
106
- try {
107
- var diagnosticsClient = _this.localConfig.diagnosticsClient;
108
- diagnosticsClient === null || diagnosticsClient === void 0 ? void 0 : diagnosticsClient.increment(diagnostics_1.SrDiagnostic.configSource(String(source)));
109
- diagnosticsClient === null || diagnosticsClient === void 0 ? void 0 : diagnosticsClient.increment(targetingConfig ? diagnostics_1.SrDiagnostic.configHasTargeting : diagnostics_1.SrDiagnostic.configNoTargeting);
110
- diagnosticsClient === null || diagnosticsClient === void 0 ? void 0 : diagnosticsClient.recordEvent(diagnostics_1.SrDiagnostic.configReceived, {
111
- sessionId: _this.sessionId,
112
- deviceId: _this.deviceId,
113
- srId: _this.deviceId != null && _this.sessionId != null ? "".concat(_this.deviceId, "/").concat(_this.sessionId) : undefined,
114
- source: String(source),
115
- hasSampling: !!samplingConfig,
116
- captureEnabled: samplingForLog === null || samplingForLog === void 0 ? void 0 : samplingForLog.capture_enabled,
117
- sampleRate: samplingForLog === null || samplingForLog === void 0 ? void 0 : samplingForLog.sample_rate,
118
- hasTargeting: !!targetingConfig,
119
- targetingSegmentCount: Array.isArray(targetingSegments) ? targetingSegments.length : undefined,
120
- hasPrivacy: !!privacyConfig,
121
- });
122
- // Flush now (vs the client's ~5-min timer). One capture POST per event — higher
123
- // volume; revisit/gate before production.
124
- void ((_b = diagnosticsClient === null || diagnosticsClient === void 0 ? void 0 : diagnosticsClient._flush) === null || _b === void 0 ? void 0 : _b.call(diagnosticsClient));
125
- }
126
- catch (_c) {
127
- // diagnostics is best-effort
128
- }
129
95
  if (samplingConfig || privacyConfig || targetingConfig) {
130
96
  sessionReplayRemoteConfig = {};
131
97
  if (samplingConfig) {
@@ -146,18 +112,11 @@ var SessionReplayJoinedConfigGenerator = /** @class */ (function () {
146
112
  // to cache after the budget elapses. 'all' mode would race a synchronous cache read
147
113
  // against the network and resolve on whichever fires first — cache always wins, so a
148
114
  // stale cache silently overrides the live config (SR-4234).
149
- _q.sent();
115
+ _m.sent();
150
116
  return [3 /*break*/, 4];
151
117
  case 3:
152
- error_1 = _q.sent();
118
+ error_1 = _m.sent();
153
119
  this.localConfig.loggerProvider.error('Failed to generate joined config: ', error_1);
154
- try {
155
- (_a = this.localConfig.diagnosticsClient) === null || _a === void 0 ? void 0 : _a.increment(diagnostics_1.SrDiagnostic.configFetchFailed);
156
- void ((_c = (_b = this.localConfig.diagnosticsClient) === null || _b === void 0 ? void 0 : _b._flush) === null || _c === void 0 ? void 0 : _c.call(_b));
157
- }
158
- catch (_r) {
159
- // diagnostics is best-effort
160
- }
161
120
  config.captureEnabled = false;
162
121
  return [2 /*return*/, {
163
122
  localConfig: this.localConfig,
@@ -208,14 +167,14 @@ var SessionReplayJoinedConfigGenerator = /** @class */ (function () {
208
167
  // ...remotePrivacyConfig,
209
168
  // };
210
169
  if (remotePrivacyConfig) {
211
- localPrivacyConfig = (_d = config.privacyConfig) !== null && _d !== void 0 ? _d : {};
170
+ localPrivacyConfig = (_a = config.privacyConfig) !== null && _a !== void 0 ? _a : {};
212
171
  joinedPrivacyConfig = {
213
- defaultMaskLevel: (_f = (_e = remotePrivacyConfig.defaultMaskLevel) !== null && _e !== void 0 ? _e : localPrivacyConfig.defaultMaskLevel) !== null && _f !== void 0 ? _f : 'medium',
172
+ defaultMaskLevel: (_c = (_b = remotePrivacyConfig.defaultMaskLevel) !== null && _b !== void 0 ? _b : localPrivacyConfig.defaultMaskLevel) !== null && _c !== void 0 ? _c : 'medium',
214
173
  blockSelector: [],
215
174
  maskSelector: [],
216
175
  unmaskSelector: [],
217
- maskAttributes: tslib_1.__spreadArray([], tslib_1.__read(new Set(tslib_1.__spreadArray(tslib_1.__spreadArray([], tslib_1.__read(((_g = localPrivacyConfig.maskAttributes) !== null && _g !== void 0 ? _g : [])), false), tslib_1.__read(((_h = remotePrivacyConfig.maskAttributes) !== null && _h !== void 0 ? _h : [])), false))), false),
218
- urlMaskLevels: tslib_1.__spreadArray(tslib_1.__spreadArray([], tslib_1.__read(((_j = remotePrivacyConfig.urlMaskLevels) !== null && _j !== void 0 ? _j : [])), false), tslib_1.__read(((_k = localPrivacyConfig.urlMaskLevels) !== null && _k !== void 0 ? _k : [])), false),
176
+ maskAttributes: tslib_1.__spreadArray([], tslib_1.__read(new Set(tslib_1.__spreadArray(tslib_1.__spreadArray([], tslib_1.__read(((_d = localPrivacyConfig.maskAttributes) !== null && _d !== void 0 ? _d : [])), false), tslib_1.__read(((_e = remotePrivacyConfig.maskAttributes) !== null && _e !== void 0 ? _e : [])), false))), false),
177
+ urlMaskLevels: tslib_1.__spreadArray(tslib_1.__spreadArray([], tslib_1.__read(((_f = remotePrivacyConfig.urlMaskLevels) !== null && _f !== void 0 ? _f : [])), false), tslib_1.__read(((_g = localPrivacyConfig.urlMaskLevels) !== null && _g !== void 0 ? _g : [])), false),
219
178
  };
220
179
  privacyConfigSelectorMap = function (privacyConfig) {
221
180
  var e_2, _a, e_3, _b, e_4, _c;
@@ -267,8 +226,8 @@ var SessionReplayJoinedConfigGenerator = /** @class */ (function () {
267
226
  };
268
227
  selectorMap = tslib_1.__assign(tslib_1.__assign({}, privacyConfigSelectorMap(localPrivacyConfig)), privacyConfigSelectorMap(remotePrivacyConfig));
269
228
  try {
270
- for (_l = tslib_1.__values(Object.entries(selectorMap)), _m = _l.next(); !_m.done; _m = _l.next()) {
271
- _o = tslib_1.__read(_m.value, 2), selector = _o[0], selectorType = _o[1];
229
+ for (_h = tslib_1.__values(Object.entries(selectorMap)), _j = _h.next(); !_j.done; _j = _h.next()) {
230
+ _k = tslib_1.__read(_j.value, 2), selector = _k[0], selectorType = _k[1];
272
231
  if (selectorType === 'mask') {
273
232
  joinedPrivacyConfig.maskSelector.push(selector);
274
233
  }
@@ -283,7 +242,7 @@ var SessionReplayJoinedConfigGenerator = /** @class */ (function () {
283
242
  catch (e_1_1) { e_1 = { error: e_1_1 }; }
284
243
  finally {
285
244
  try {
286
- if (_m && !_m.done && (_p = _l.return)) _p.call(_l);
245
+ if (_j && !_j.done && (_l = _h.return)) _l.call(_h);
287
246
  }
288
247
  finally { if (e_1) throw e_1.error; }
289
248
  }
@@ -338,11 +297,11 @@ var createSessionReplayJoinedConfigGenerator = function (apiKey, options) { retu
338
297
  var localConfig, remoteConfigClient;
339
298
  return tslib_1.__generator(this, function (_a) {
340
299
  localConfig = new local_config_1.SessionReplayLocalConfig(apiKey, options);
341
- remoteConfigClient = new analytics_core_1.RemoteConfigClient(apiKey, localConfig.loggerProvider, localConfig.serverZone, options.configServerUrl);
342
- return [2 /*return*/, new SessionReplayJoinedConfigGenerator(remoteConfigClient, localConfig, {
343
- sessionId: options.sessionId,
344
- deviceId: options.deviceId,
345
- })];
300
+ remoteConfigClient = new analytics_core_1.RemoteConfigClient(apiKey, localConfig.loggerProvider, localConfig.serverZone, options.configServerUrl,
301
+ // Custom transport for the config GET. SR's FetchConfigRequest is structurally compatible
302
+ // with RemoteConfigFetchRequest, so the customer's handleFetchConfig drops straight in.
303
+ options.handleFetchConfig);
304
+ return [2 /*return*/, new SessionReplayJoinedConfigGenerator(remoteConfigClient, localConfig)];
346
305
  });
347
306
  }); };
348
307
  exports.createSessionReplayJoinedConfigGenerator = createSessionReplayJoinedConfigGenerator;
@@ -1 +1 @@
1
- {"version":3,"file":"joined-config.js","sourceRoot":"","sources":["../../../src/config/joined-config.ts"],"names":[],"mappings":"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","sourcesContent":["import { ILogger, IRemoteConfigClient, RemoteConfigClient, RemoteConfig, Source } from '@amplitude/analytics-core';\nimport { getDebugConfig } from '../helpers';\nimport { SrDiagnostic } from '../diagnostics';\nimport { SessionReplayOptions } from '../typings/session-replay';\nimport { SessionReplayLocalConfig } from './local-config';\nimport {\n SessionReplayLocalConfig as ISessionReplayLocalConfig,\n PrivacyConfig,\n SessionReplayConfigs,\n SessionReplayJoinedConfig,\n SessionReplayRemoteConfig,\n} from './types';\n\n// Budget for waiting on the remote config response before falling back to the local cache.\n// The inner fetch in analytics-core uses a 1000ms per-attempt AbortController timeout with\n// up to 3 retries — but this outer timeout is the only thing the SR plugin waits on, so\n// only the first attempt can possibly complete in time (attempt 2 starts at ~1000-1333ms\n// after backoff jitter and runs to ~2000-2333ms, well past any reasonable outer budget).\n// 1500ms is set above the 1000ms inner abort to avoid a tie with the inner cutoff and\n// give the first attempt's resolution path room to finish; everything else falls through\n// to the cache fallback. See SR-4234: prefer remote over a stale cache that would\n// otherwise win the synchronous race in 'all' mode.\nconst REMOTE_CONFIG_TIMEOUT_MS = 1500;\n\nexport const removeInvalidSelectorsFromPrivacyConfig = (privacyConfig: PrivacyConfig, loggerProvider: ILogger) => {\n // This allows us to not search the DOM.\n const fragment = document.createDocumentFragment();\n\n const dropInvalidSelectors = (selectors: string[] | string = []): string[] | undefined => {\n if (typeof selectors === 'string') {\n selectors = [selectors];\n }\n selectors = selectors.filter((selector: string) => {\n try {\n fragment.querySelector(selector);\n } catch {\n loggerProvider.warn(`[session-replay-browser] omitting selector \"${selector}\" because it is invalid`);\n return false;\n }\n return true;\n });\n if (selectors.length === 0) {\n return undefined;\n }\n return selectors;\n };\n privacyConfig.blockSelector = dropInvalidSelectors(privacyConfig.blockSelector);\n privacyConfig.maskSelector = dropInvalidSelectors(privacyConfig.maskSelector);\n privacyConfig.unmaskSelector = dropInvalidSelectors(privacyConfig.unmaskSelector);\n return privacyConfig;\n};\nexport class SessionReplayJoinedConfigGenerator {\n private readonly localConfig: ISessionReplayLocalConfig;\n private readonly remoteConfigClient: IRemoteConfigClient;\n // Identity for diagnostics correlation (config fetch is per-session). Sourced from init options.\n private readonly sessionId?: string | number;\n private readonly deviceId?: string;\n\n constructor(\n remoteConfigClient: IRemoteConfigClient,\n localConfig: ISessionReplayLocalConfig,\n identity?: { sessionId?: string | number; deviceId?: string },\n ) {\n this.localConfig = localConfig;\n this.remoteConfigClient = remoteConfigClient;\n this.sessionId = identity?.sessionId;\n this.deviceId = identity?.deviceId;\n }\n\n async generateJoinedConfig(): Promise<SessionReplayConfigs> {\n const config: SessionReplayJoinedConfig = { ...this.localConfig };\n // Special case here as optOut is implemented via getter/setter\n config.optOut = this.localConfig.optOut;\n // We always want captureEnabled to be true, unless there's an override\n // in the remote config.\n config.captureEnabled = true;\n let sessionReplayRemoteConfig: SessionReplayRemoteConfig | undefined;\n\n try {\n // Subscribe with a timeout so the SDK prefers the remote response and only falls back\n // to cache after the budget elapses. 'all' mode would race a synchronous cache read\n // against the network and resolve on whichever fires first — cache always wins, so a\n // stale cache silently overrides the live config (SR-4234).\n await new Promise<void>((resolve, reject) => {\n this.remoteConfigClient.subscribe(\n 'configs.sessionReplay',\n { timeout: REMOTE_CONFIG_TIMEOUT_MS },\n (remoteConfig: RemoteConfig | null, source: Source) => {\n this.localConfig.loggerProvider.debug(\n `Session Replay remote configuration received from ${source}:`,\n JSON.stringify(remoteConfig, null, 2),\n );\n\n if (!remoteConfig) {\n reject(new Error('No remote config received'));\n return;\n }\n\n // remoteConfig is already filtered to 'configs.sessionReplay' namespace\n const namespaceConfig = remoteConfig as SessionReplayRemoteConfig;\n const samplingConfig = namespaceConfig.sr_sampling_config;\n const privacyConfig = namespaceConfig.sr_privacy_config;\n const targetingConfig = namespaceConfig.sr_targeting_config;\n\n // Captured for the sr.trc.config.received diagnostics event below (remote vs cache is\n // the crux of SR-4234 stale-cache reports).\n const samplingForLog = samplingConfig as { capture_enabled?: boolean; sample_rate?: number } | undefined;\n const targetingSegments = (targetingConfig as unknown as { segments?: unknown[] } | undefined)?.segments;\n\n const ugcFilterRules = config.interactionConfig?.ugcFilterRules;\n // This is intentionally forced to only be set through the remote config.\n config.interactionConfig = namespaceConfig.sr_interaction_config;\n if (config.interactionConfig && ugcFilterRules) {\n config.interactionConfig.ugcFilterRules = ugcFilterRules;\n }\n\n // This is intentionally forced to only be set through the remote config.\n config.loggingConfig = namespaceConfig.sr_logging_config;\n\n // Team-visible diagnostics: which SOURCE the config came from (remote vs cache — the\n // crux of SR-4234) and whether targeting was present. Counters aggregate across the\n // customer's sessions; the event carries the full context (source, sample rate,\n // segment count) as queryable log fields. No-op when diagnostics isn't configured.\n try {\n const diagnosticsClient = this.localConfig.diagnosticsClient;\n diagnosticsClient?.increment(SrDiagnostic.configSource(String(source)));\n diagnosticsClient?.increment(\n targetingConfig ? SrDiagnostic.configHasTargeting : SrDiagnostic.configNoTargeting,\n );\n diagnosticsClient?.recordEvent(SrDiagnostic.configReceived, {\n sessionId: this.sessionId,\n deviceId: this.deviceId,\n srId:\n this.deviceId != null && this.sessionId != null ? `${this.deviceId}/${this.sessionId}` : undefined,\n source: String(source),\n hasSampling: !!samplingConfig,\n captureEnabled: samplingForLog?.capture_enabled,\n sampleRate: samplingForLog?.sample_rate,\n hasTargeting: !!targetingConfig,\n targetingSegmentCount: Array.isArray(targetingSegments) ? targetingSegments.length : undefined,\n hasPrivacy: !!privacyConfig,\n });\n // Flush now (vs the client's ~5-min timer). One capture POST per event — higher\n // volume; revisit/gate before production.\n void diagnosticsClient?._flush?.();\n } catch {\n // diagnostics is best-effort\n }\n\n if (samplingConfig || privacyConfig || targetingConfig) {\n sessionReplayRemoteConfig = {};\n if (samplingConfig) {\n sessionReplayRemoteConfig.sr_sampling_config = samplingConfig;\n }\n if (privacyConfig) {\n sessionReplayRemoteConfig.sr_privacy_config = privacyConfig;\n }\n if (targetingConfig) {\n sessionReplayRemoteConfig.sr_targeting_config = targetingConfig;\n }\n }\n\n resolve();\n },\n );\n });\n } catch (error) {\n this.localConfig.loggerProvider.error('Failed to generate joined config: ', error);\n try {\n this.localConfig.diagnosticsClient?.increment(SrDiagnostic.configFetchFailed);\n void this.localConfig.diagnosticsClient?._flush?.();\n } catch {\n // diagnostics is best-effort\n }\n config.captureEnabled = false;\n return {\n localConfig: this.localConfig,\n joinedConfig: config,\n remoteConfig: undefined,\n };\n }\n\n if (!sessionReplayRemoteConfig) {\n return {\n localConfig: this.localConfig,\n joinedConfig: config,\n remoteConfig: sessionReplayRemoteConfig,\n };\n }\n\n const {\n sr_sampling_config: samplingConfig,\n sr_privacy_config: remotePrivacyConfig,\n sr_targeting_config: targetingConfig,\n } = sessionReplayRemoteConfig;\n if (samplingConfig && Object.keys(samplingConfig).length > 0) {\n if (Object.prototype.hasOwnProperty.call(samplingConfig, 'capture_enabled')) {\n config.captureEnabled = samplingConfig.capture_enabled;\n } else {\n config.captureEnabled = false;\n }\n\n if (Object.prototype.hasOwnProperty.call(samplingConfig, 'sample_rate')) {\n config.sampleRate = samplingConfig.sample_rate;\n }\n\n if (Object.prototype.hasOwnProperty.call(samplingConfig, 'min_session_duration_ms')) {\n config.minSessionDurationMs = this.sanitizeMinSessionDurationMs(samplingConfig.min_session_duration_ms);\n }\n } else {\n // If config API response was valid (ie 200), but no config returned, assume that\n // customer has not yet set up config, and use sample rate from SDK options,\n // allowing for immediate replay capture\n config.captureEnabled = true;\n this.localConfig.loggerProvider.debug(\n 'Remote config successfully fetched, but no values set for project, Session Replay capture enabled.',\n );\n }\n\n // Remote config join acts somewhat like a left join between the remote and the local\n // config. That is, remote config has precedence over local values as with sampling.\n // However, non conflicting values will be added to the lists.\n // Here's an example to illustrate:\n //\n // Remote config: {'.selector1': 'MASK', '.selector2': 'UNMASK'}\n // Local config: {'.selector1': 'UNMASK', '.selector3': 'MASK'}\n //\n // Resolved config: {'.selector1': 'MASK', '.selector2': 'UNMASK', '.selector3': 'MASK'}\n // config.privacyConfig = {\n // ...(config.privacyConfig ?? {}),\n // ...remotePrivacyConfig,\n // };\n\n if (remotePrivacyConfig) {\n const localPrivacyConfig: PrivacyConfig = config.privacyConfig ?? {};\n\n const joinedPrivacyConfig: Required<PrivacyConfig> & { blockSelector: string[] } = {\n defaultMaskLevel: remotePrivacyConfig.defaultMaskLevel ?? localPrivacyConfig.defaultMaskLevel ?? 'medium',\n blockSelector: [],\n maskSelector: [],\n unmaskSelector: [],\n maskAttributes: [\n ...new Set([...(localPrivacyConfig.maskAttributes ?? []), ...(remotePrivacyConfig.maskAttributes ?? [])]),\n ],\n urlMaskLevels: [...(remotePrivacyConfig.urlMaskLevels ?? []), ...(localPrivacyConfig.urlMaskLevels ?? [])],\n };\n\n const privacyConfigSelectorMap = (privacyConfig: PrivacyConfig): Record<string, 'mask' | 'unmask' | 'block'> => {\n const selectorMap: Record<string, 'mask' | 'unmask' | 'block'> = {};\n if (typeof privacyConfig.blockSelector === 'string') {\n privacyConfig.blockSelector = [privacyConfig.blockSelector];\n }\n\n for (const selector of privacyConfig.blockSelector ?? []) {\n selectorMap[selector] = 'block';\n }\n for (const selector of privacyConfig.maskSelector ?? []) {\n selectorMap[selector] = 'mask';\n }\n for (const selector of privacyConfig.unmaskSelector ?? []) {\n selectorMap[selector] = 'unmask';\n }\n return selectorMap;\n };\n\n const selectorMap: Record<string, 'mask' | 'unmask' | 'block'> = {\n ...privacyConfigSelectorMap(localPrivacyConfig),\n ...privacyConfigSelectorMap(remotePrivacyConfig),\n };\n\n for (const [selector, selectorType] of Object.entries(selectorMap)) {\n if (selectorType === 'mask') {\n joinedPrivacyConfig.maskSelector.push(selector);\n } else if (selectorType === 'block') {\n joinedPrivacyConfig.blockSelector.push(selector);\n } else if (selectorType === 'unmask') {\n joinedPrivacyConfig.unmaskSelector.push(selector);\n }\n }\n\n config.privacyConfig = removeInvalidSelectorsFromPrivacyConfig(\n joinedPrivacyConfig,\n this.localConfig.loggerProvider,\n );\n }\n\n if (targetingConfig && Object.keys(targetingConfig).length > 0) {\n config.targetingConfig = targetingConfig;\n }\n\n this.localConfig.loggerProvider.debug(\n JSON.stringify({ name: 'session replay joined config', config: getDebugConfig(config) }, null, 2),\n );\n\n return {\n localConfig: this.localConfig,\n joinedConfig: config,\n remoteConfig: sessionReplayRemoteConfig,\n };\n }\n\n /**\n * Defensive bounds for the remote-supplied min_session_duration_ms. A misconfigured\n * value (e.g. 30_000_000) would silently suppress every replay until the config is\n * pushed again, so we clamp to a sane ceiling and warn on out-of-range inputs.\n * Returns undefined for clearly invalid values so the gate falls back to disabled\n * rather than carrying a NaN through downstream comparisons.\n */\n private sanitizeMinSessionDurationMs(raw: unknown): number | undefined {\n if (typeof raw !== 'number' || !Number.isFinite(raw)) {\n this.localConfig.loggerProvider.warn(\n `min_session_duration_ms remote value is not a finite number (got ${String(raw)}); ignoring.`,\n );\n return undefined;\n }\n if (raw < 0) {\n this.localConfig.loggerProvider.warn(`min_session_duration_ms remote value is negative (${raw}); ignoring.`);\n return undefined;\n }\n if (raw > MAX_MIN_SESSION_DURATION_MS) {\n this.localConfig.loggerProvider.warn(\n `min_session_duration_ms remote value ${raw} exceeds ${MAX_MIN_SESSION_DURATION_MS}ms ceiling; clamping.`,\n );\n return MAX_MIN_SESSION_DURATION_MS;\n }\n return raw;\n }\n}\n\n/**\n * Upper bound for the remote-configured replay min duration. 60 seconds is well above\n * any reasonable bounce threshold; values higher than this are almost certainly typos\n * (e.g. seconds confused for milliseconds, or an extra zero) and would otherwise\n * suppress every replay until the config is corrected.\n */\nexport const MAX_MIN_SESSION_DURATION_MS = 60_000;\n\nexport const createSessionReplayJoinedConfigGenerator = async (apiKey: string, options: SessionReplayOptions) => {\n const localConfig = new SessionReplayLocalConfig(apiKey, options);\n\n const remoteConfigClient = new RemoteConfigClient(\n apiKey,\n localConfig.loggerProvider,\n localConfig.serverZone,\n options.configServerUrl,\n );\n\n return new SessionReplayJoinedConfigGenerator(remoteConfigClient, localConfig, {\n sessionId: options.sessionId,\n deviceId: options.deviceId,\n });\n};\n"]}
1
+ {"version":3,"file":"joined-config.js","sourceRoot":"","sources":["../../../src/config/joined-config.ts"],"names":[],"mappings":"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","sourcesContent":["import { ILogger, IRemoteConfigClient, RemoteConfigClient, RemoteConfig, Source } from '@amplitude/analytics-core';\nimport { getDebugConfig } from '../helpers';\nimport { SessionReplayOptions } from '../typings/session-replay';\nimport { SessionReplayLocalConfig } from './local-config';\nimport {\n SessionReplayLocalConfig as ISessionReplayLocalConfig,\n PrivacyConfig,\n SessionReplayConfigs,\n SessionReplayJoinedConfig,\n SessionReplayRemoteConfig,\n} from './types';\n\n// Budget for waiting on the remote config response before falling back to the local cache.\n// The inner fetch in analytics-core uses a 1000ms per-attempt AbortController timeout with\n// up to 3 retries — but this outer timeout is the only thing the SR plugin waits on, so\n// only the first attempt can possibly complete in time (attempt 2 starts at ~1000-1333ms\n// after backoff jitter and runs to ~2000-2333ms, well past any reasonable outer budget).\n// 1500ms is set above the 1000ms inner abort to avoid a tie with the inner cutoff and\n// give the first attempt's resolution path room to finish; everything else falls through\n// to the cache fallback. See SR-4234: prefer remote over a stale cache that would\n// otherwise win the synchronous race in 'all' mode.\nconst REMOTE_CONFIG_TIMEOUT_MS = 1500;\n\nexport const removeInvalidSelectorsFromPrivacyConfig = (privacyConfig: PrivacyConfig, loggerProvider: ILogger) => {\n // This allows us to not search the DOM.\n const fragment = document.createDocumentFragment();\n\n const dropInvalidSelectors = (selectors: string[] | string = []): string[] | undefined => {\n if (typeof selectors === 'string') {\n selectors = [selectors];\n }\n selectors = selectors.filter((selector: string) => {\n try {\n fragment.querySelector(selector);\n } catch {\n loggerProvider.warn(`[session-replay-browser] omitting selector \"${selector}\" because it is invalid`);\n return false;\n }\n return true;\n });\n if (selectors.length === 0) {\n return undefined;\n }\n return selectors;\n };\n privacyConfig.blockSelector = dropInvalidSelectors(privacyConfig.blockSelector);\n privacyConfig.maskSelector = dropInvalidSelectors(privacyConfig.maskSelector);\n privacyConfig.unmaskSelector = dropInvalidSelectors(privacyConfig.unmaskSelector);\n return privacyConfig;\n};\nexport class SessionReplayJoinedConfigGenerator {\n private readonly localConfig: ISessionReplayLocalConfig;\n private readonly remoteConfigClient: IRemoteConfigClient;\n\n constructor(remoteConfigClient: IRemoteConfigClient, localConfig: ISessionReplayLocalConfig) {\n this.localConfig = localConfig;\n this.remoteConfigClient = remoteConfigClient;\n }\n\n async generateJoinedConfig(): Promise<SessionReplayConfigs> {\n const config: SessionReplayJoinedConfig = { ...this.localConfig };\n // Special case here as optOut is implemented via getter/setter\n config.optOut = this.localConfig.optOut;\n // We always want captureEnabled to be true, unless there's an override\n // in the remote config.\n config.captureEnabled = true;\n let sessionReplayRemoteConfig: SessionReplayRemoteConfig | undefined;\n\n try {\n // Subscribe with a timeout so the SDK prefers the remote response and only falls back\n // to cache after the budget elapses. 'all' mode would race a synchronous cache read\n // against the network and resolve on whichever fires first — cache always wins, so a\n // stale cache silently overrides the live config (SR-4234).\n await new Promise<void>((resolve, reject) => {\n this.remoteConfigClient.subscribe(\n 'configs.sessionReplay',\n { timeout: REMOTE_CONFIG_TIMEOUT_MS },\n (remoteConfig: RemoteConfig | null, source: Source) => {\n this.localConfig.loggerProvider.debug(\n `Session Replay remote configuration received from ${source}:`,\n JSON.stringify(remoteConfig, null, 2),\n );\n\n if (!remoteConfig) {\n reject(new Error('No remote config received'));\n return;\n }\n\n // remoteConfig is already filtered to 'configs.sessionReplay' namespace\n const namespaceConfig = remoteConfig as SessionReplayRemoteConfig;\n const samplingConfig = namespaceConfig.sr_sampling_config;\n const privacyConfig = namespaceConfig.sr_privacy_config;\n const targetingConfig = namespaceConfig.sr_targeting_config;\n\n const ugcFilterRules = config.interactionConfig?.ugcFilterRules;\n // This is intentionally forced to only be set through the remote config.\n config.interactionConfig = namespaceConfig.sr_interaction_config;\n if (config.interactionConfig && ugcFilterRules) {\n config.interactionConfig.ugcFilterRules = ugcFilterRules;\n }\n\n // This is intentionally forced to only be set through the remote config.\n config.loggingConfig = namespaceConfig.sr_logging_config;\n\n if (samplingConfig || privacyConfig || targetingConfig) {\n sessionReplayRemoteConfig = {};\n if (samplingConfig) {\n sessionReplayRemoteConfig.sr_sampling_config = samplingConfig;\n }\n if (privacyConfig) {\n sessionReplayRemoteConfig.sr_privacy_config = privacyConfig;\n }\n if (targetingConfig) {\n sessionReplayRemoteConfig.sr_targeting_config = targetingConfig;\n }\n }\n\n resolve();\n },\n );\n });\n } catch (error) {\n this.localConfig.loggerProvider.error('Failed to generate joined config: ', error);\n config.captureEnabled = false;\n return {\n localConfig: this.localConfig,\n joinedConfig: config,\n remoteConfig: undefined,\n };\n }\n\n if (!sessionReplayRemoteConfig) {\n return {\n localConfig: this.localConfig,\n joinedConfig: config,\n remoteConfig: sessionReplayRemoteConfig,\n };\n }\n\n const {\n sr_sampling_config: samplingConfig,\n sr_privacy_config: remotePrivacyConfig,\n sr_targeting_config: targetingConfig,\n } = sessionReplayRemoteConfig;\n if (samplingConfig && Object.keys(samplingConfig).length > 0) {\n if (Object.prototype.hasOwnProperty.call(samplingConfig, 'capture_enabled')) {\n config.captureEnabled = samplingConfig.capture_enabled;\n } else {\n config.captureEnabled = false;\n }\n\n if (Object.prototype.hasOwnProperty.call(samplingConfig, 'sample_rate')) {\n config.sampleRate = samplingConfig.sample_rate;\n }\n\n if (Object.prototype.hasOwnProperty.call(samplingConfig, 'min_session_duration_ms')) {\n config.minSessionDurationMs = this.sanitizeMinSessionDurationMs(samplingConfig.min_session_duration_ms);\n }\n } else {\n // If config API response was valid (ie 200), but no config returned, assume that\n // customer has not yet set up config, and use sample rate from SDK options,\n // allowing for immediate replay capture\n config.captureEnabled = true;\n this.localConfig.loggerProvider.debug(\n 'Remote config successfully fetched, but no values set for project, Session Replay capture enabled.',\n );\n }\n\n // Remote config join acts somewhat like a left join between the remote and the local\n // config. That is, remote config has precedence over local values as with sampling.\n // However, non conflicting values will be added to the lists.\n // Here's an example to illustrate:\n //\n // Remote config: {'.selector1': 'MASK', '.selector2': 'UNMASK'}\n // Local config: {'.selector1': 'UNMASK', '.selector3': 'MASK'}\n //\n // Resolved config: {'.selector1': 'MASK', '.selector2': 'UNMASK', '.selector3': 'MASK'}\n // config.privacyConfig = {\n // ...(config.privacyConfig ?? {}),\n // ...remotePrivacyConfig,\n // };\n\n if (remotePrivacyConfig) {\n const localPrivacyConfig: PrivacyConfig = config.privacyConfig ?? {};\n\n const joinedPrivacyConfig: Required<PrivacyConfig> & { blockSelector: string[] } = {\n defaultMaskLevel: remotePrivacyConfig.defaultMaskLevel ?? localPrivacyConfig.defaultMaskLevel ?? 'medium',\n blockSelector: [],\n maskSelector: [],\n unmaskSelector: [],\n maskAttributes: [\n ...new Set([...(localPrivacyConfig.maskAttributes ?? []), ...(remotePrivacyConfig.maskAttributes ?? [])]),\n ],\n urlMaskLevels: [...(remotePrivacyConfig.urlMaskLevels ?? []), ...(localPrivacyConfig.urlMaskLevels ?? [])],\n };\n\n const privacyConfigSelectorMap = (privacyConfig: PrivacyConfig): Record<string, 'mask' | 'unmask' | 'block'> => {\n const selectorMap: Record<string, 'mask' | 'unmask' | 'block'> = {};\n if (typeof privacyConfig.blockSelector === 'string') {\n privacyConfig.blockSelector = [privacyConfig.blockSelector];\n }\n\n for (const selector of privacyConfig.blockSelector ?? []) {\n selectorMap[selector] = 'block';\n }\n for (const selector of privacyConfig.maskSelector ?? []) {\n selectorMap[selector] = 'mask';\n }\n for (const selector of privacyConfig.unmaskSelector ?? []) {\n selectorMap[selector] = 'unmask';\n }\n return selectorMap;\n };\n\n const selectorMap: Record<string, 'mask' | 'unmask' | 'block'> = {\n ...privacyConfigSelectorMap(localPrivacyConfig),\n ...privacyConfigSelectorMap(remotePrivacyConfig),\n };\n\n for (const [selector, selectorType] of Object.entries(selectorMap)) {\n if (selectorType === 'mask') {\n joinedPrivacyConfig.maskSelector.push(selector);\n } else if (selectorType === 'block') {\n joinedPrivacyConfig.blockSelector.push(selector);\n } else if (selectorType === 'unmask') {\n joinedPrivacyConfig.unmaskSelector.push(selector);\n }\n }\n\n config.privacyConfig = removeInvalidSelectorsFromPrivacyConfig(\n joinedPrivacyConfig,\n this.localConfig.loggerProvider,\n );\n }\n\n if (targetingConfig && Object.keys(targetingConfig).length > 0) {\n config.targetingConfig = targetingConfig;\n }\n\n this.localConfig.loggerProvider.debug(\n JSON.stringify({ name: 'session replay joined config', config: getDebugConfig(config) }, null, 2),\n );\n\n return {\n localConfig: this.localConfig,\n joinedConfig: config,\n remoteConfig: sessionReplayRemoteConfig,\n };\n }\n\n /**\n * Defensive bounds for the remote-supplied min_session_duration_ms. A misconfigured\n * value (e.g. 30_000_000) would silently suppress every replay until the config is\n * pushed again, so we clamp to a sane ceiling and warn on out-of-range inputs.\n * Returns undefined for clearly invalid values so the gate falls back to disabled\n * rather than carrying a NaN through downstream comparisons.\n */\n private sanitizeMinSessionDurationMs(raw: unknown): number | undefined {\n if (typeof raw !== 'number' || !Number.isFinite(raw)) {\n this.localConfig.loggerProvider.warn(\n `min_session_duration_ms remote value is not a finite number (got ${String(raw)}); ignoring.`,\n );\n return undefined;\n }\n if (raw < 0) {\n this.localConfig.loggerProvider.warn(`min_session_duration_ms remote value is negative (${raw}); ignoring.`);\n return undefined;\n }\n if (raw > MAX_MIN_SESSION_DURATION_MS) {\n this.localConfig.loggerProvider.warn(\n `min_session_duration_ms remote value ${raw} exceeds ${MAX_MIN_SESSION_DURATION_MS}ms ceiling; clamping.`,\n );\n return MAX_MIN_SESSION_DURATION_MS;\n }\n return raw;\n }\n}\n\n/**\n * Upper bound for the remote-configured replay min duration. 60 seconds is well above\n * any reasonable bounce threshold; values higher than this are almost certainly typos\n * (e.g. seconds confused for milliseconds, or an extra zero) and would otherwise\n * suppress every replay until the config is corrected.\n */\nexport const MAX_MIN_SESSION_DURATION_MS = 60_000;\n\nexport const createSessionReplayJoinedConfigGenerator = async (apiKey: string, options: SessionReplayOptions) => {\n const localConfig = new SessionReplayLocalConfig(apiKey, options);\n\n const remoteConfigClient = new RemoteConfigClient(\n apiKey,\n localConfig.loggerProvider,\n localConfig.serverZone,\n options.configServerUrl,\n // Custom transport for the config GET. SR's FetchConfigRequest is structurally compatible\n // with RemoteConfigFetchRequest, so the customer's handleFetchConfig drops straight in.\n options.handleFetchConfig,\n );\n\n return new SessionReplayJoinedConfigGenerator(remoteConfigClient, localConfig);\n};\n"]}