@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.
- package/README.md +203 -0
- package/lib/cjs/beacon-transport.d.ts +2 -0
- package/lib/cjs/beacon-transport.d.ts.map +1 -1
- package/lib/cjs/beacon-transport.js +39 -1
- package/lib/cjs/beacon-transport.js.map +1 -1
- package/lib/cjs/config/joined-config.d.ts +1 -6
- package/lib/cjs/config/joined-config.d.ts.map +1 -1
- package/lib/cjs/config/joined-config.js +23 -64
- package/lib/cjs/config/joined-config.js.map +1 -1
- package/lib/cjs/config/local-config.d.ts +4 -6
- package/lib/cjs/config/local-config.d.ts.map +1 -1
- package/lib/cjs/config/local-config.js +3 -19
- package/lib/cjs/config/local-config.js.map +1 -1
- package/lib/cjs/config/types.d.ts +74 -19
- package/lib/cjs/config/types.d.ts.map +1 -1
- package/lib/cjs/config/types.js.map +1 -1
- package/lib/cjs/index.d.ts +1 -0
- package/lib/cjs/index.d.ts.map +1 -1
- package/lib/cjs/index.js.map +1 -1
- package/lib/cjs/plugins/url-tracking-plugin.d.ts +0 -13
- package/lib/cjs/plugins/url-tracking-plugin.d.ts.map +1 -1
- package/lib/cjs/plugins/url-tracking-plugin.js +2 -6
- package/lib/cjs/plugins/url-tracking-plugin.js.map +1 -1
- package/lib/cjs/session-replay.d.ts +0 -33
- package/lib/cjs/session-replay.d.ts.map +1 -1
- package/lib/cjs/session-replay.js +40 -335
- package/lib/cjs/session-replay.js.map +1 -1
- package/lib/cjs/targeting/targeting-manager.d.ts +1 -4
- package/lib/cjs/targeting/targeting-manager.d.ts.map +1 -1
- package/lib/cjs/targeting/targeting-manager.js +10 -56
- package/lib/cjs/targeting/targeting-manager.js.map +1 -1
- package/lib/cjs/track-destination.d.ts +5 -1
- package/lib/cjs/track-destination.d.ts.map +1 -1
- package/lib/cjs/track-destination.js +212 -55
- package/lib/cjs/track-destination.js.map +1 -1
- package/lib/cjs/version.d.ts +1 -1
- package/lib/cjs/version.d.ts.map +1 -1
- package/lib/cjs/version.js +1 -1
- package/lib/cjs/version.js.map +1 -1
- package/lib/cjs/worker/index.js +1 -1
- package/lib/esm/beacon-transport.d.ts +2 -0
- package/lib/esm/beacon-transport.d.ts.map +1 -1
- package/lib/esm/beacon-transport.js +39 -1
- package/lib/esm/beacon-transport.js.map +1 -1
- package/lib/esm/config/joined-config.d.ts +1 -6
- package/lib/esm/config/joined-config.d.ts.map +1 -1
- package/lib/esm/config/joined-config.js +23 -64
- package/lib/esm/config/joined-config.js.map +1 -1
- package/lib/esm/config/local-config.d.ts +4 -6
- package/lib/esm/config/local-config.d.ts.map +1 -1
- package/lib/esm/config/local-config.js +4 -20
- package/lib/esm/config/local-config.js.map +1 -1
- package/lib/esm/config/types.d.ts +74 -19
- package/lib/esm/config/types.d.ts.map +1 -1
- package/lib/esm/config/types.js.map +1 -1
- package/lib/esm/index.d.ts +1 -0
- package/lib/esm/index.d.ts.map +1 -1
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/plugins/url-tracking-plugin.d.ts +0 -13
- package/lib/esm/plugins/url-tracking-plugin.d.ts.map +1 -1
- package/lib/esm/plugins/url-tracking-plugin.js +2 -6
- package/lib/esm/plugins/url-tracking-plugin.js.map +1 -1
- package/lib/esm/session-replay.d.ts +0 -33
- package/lib/esm/session-replay.d.ts.map +1 -1
- package/lib/esm/session-replay.js +40 -335
- package/lib/esm/session-replay.js.map +1 -1
- package/lib/esm/targeting/targeting-manager.d.ts +1 -4
- package/lib/esm/targeting/targeting-manager.d.ts.map +1 -1
- package/lib/esm/targeting/targeting-manager.js +10 -56
- package/lib/esm/targeting/targeting-manager.js.map +1 -1
- package/lib/esm/track-destination.d.ts +5 -1
- package/lib/esm/track-destination.d.ts.map +1 -1
- package/lib/esm/track-destination.js +212 -55
- package/lib/esm/track-destination.js.map +1 -1
- package/lib/esm/version.d.ts +1 -1
- package/lib/esm/version.d.ts.map +1 -1
- package/lib/esm/version.js +1 -1
- package/lib/esm/version.js.map +1 -1
- package/lib/esm/worker/index.js +1 -1
- package/lib/scripts/index-min.js +1 -1
- package/lib/scripts/index-min.js.gz +0 -0
- package/lib/scripts/index-min.js.map +1 -1
- package/lib/scripts/observers-min.js +1 -1
- package/lib/scripts/observers-min.js.gz +0 -0
- package/lib/scripts/session-replay-browser-min.js +1 -1
- package/lib/scripts/session-replay-browser-min.js.gz +0 -0
- package/lib/scripts/session-replay-browser-min.js.map +1 -1
- package/lib/scripts/targeting-min.js +1 -1
- package/lib/scripts/targeting-min.js.gz +0 -0
- package/lib/scripts/worker-min.js +1 -1
- package/lib/scripts/worker-min.js.gz +0 -0
- package/package.json +4 -4
- package/lib/cjs/diagnostics.d.ts +0 -50
- package/lib/cjs/diagnostics.d.ts.map +0 -1
- package/lib/cjs/diagnostics.js +0 -67
- package/lib/cjs/diagnostics.js.map +0 -1
- package/lib/esm/diagnostics.d.ts +0 -50
- package/lib/esm/diagnostics.d.ts.map +0 -1
- package/lib/esm/diagnostics.js +0 -64
- 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,
|
|
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
|
|
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,
|
|
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
|
-
|
|
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;
|
|
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
|
|
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
|
|
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,
|
|
59
|
-
var e_1,
|
|
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 (
|
|
62
|
-
switch (
|
|
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
|
-
|
|
67
|
+
_m.label = 1;
|
|
71
68
|
case 1:
|
|
72
|
-
|
|
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
|
|
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
|
-
|
|
115
|
+
_m.sent();
|
|
150
116
|
return [3 /*break*/, 4];
|
|
151
117
|
case 3:
|
|
152
|
-
error_1 =
|
|
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 = (
|
|
170
|
+
localPrivacyConfig = (_a = config.privacyConfig) !== null && _a !== void 0 ? _a : {};
|
|
212
171
|
joinedPrivacyConfig = {
|
|
213
|
-
defaultMaskLevel: (
|
|
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(((
|
|
218
|
-
urlMaskLevels: tslib_1.__spreadArray(tslib_1.__spreadArray([], tslib_1.__read(((
|
|
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 (
|
|
271
|
-
|
|
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 (
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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"]}
|