@corbado/observe 1.8.0 → 1.9.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/dist/analysis/client.mjs +82 -56
- package/dist/analysis/config-manager.mjs +30 -5
- package/dist/analysis/device-info-manager.mjs +16 -1
- package/dist/analysis/device.mjs +88 -10
- package/dist/analysis/handoff.mjs +7 -0
- package/dist/analysis/low-events/grouped-input-tracker.mjs +148 -33
- package/dist/analysis/low-events/input-tracker.mjs +138 -59
- package/dist/analysis/low-events/shared-window-observer.mjs +148 -77
- package/dist/analysis/low-events/viewport-batcher.mjs +3 -3
- package/dist/analysis/queue.mjs +75 -8
- package/dist/analysis/{low-events/input-paste-observer.mjs → readers/input-paste.mjs} +2 -2
- package/dist/analysis/readers/input.mjs +61 -0
- package/dist/analysis/readers/loader-global.mjs +15 -0
- package/dist/analysis/readers/network.mjs +14 -0
- package/dist/analysis/readers/page.mjs +30 -0
- package/dist/analysis/readers/storage.mjs +28 -0
- package/dist/analysis/{utils → readers}/web-lock.mjs +6 -2
- package/dist/analysis/readers/window-events.mjs +48 -0
- package/dist/analysis/storage/index.mjs +61 -9
- package/dist/analysis/store/event-store.mjs +34 -16
- package/dist/analysis/transports/fetch.mjs +45 -3
- package/dist/analysis/utils/email-link-operation-full.mjs +22 -2
- package/dist/analysis/utils/email-otp-operation-full.mjs +0 -1
- package/dist/analysis/utils/form-operation-full.mjs +0 -1
- package/dist/analysis/utils/operation-full-provide-identifier-with-cui.mjs +6 -6
- package/dist/analysis/utils/passkey-enrollment-operation-full.mjs +9 -6
- package/dist/analysis/utils/passkey-login-operation-full.mjs +10 -5
- package/dist/analysis/utils/password-login-operation-full.mjs +8 -4
- package/dist/analysis/utils/password-manager-sampler.mjs +44 -4
- package/dist/analysis/utils/sms-otp-operation-full.mjs +0 -1
- package/dist/analysis/utils/totp-operation-full.mjs +0 -1
- package/dist/analysis/version.mjs +1 -1
- package/dist/cdn/script.global.js +4 -4
- package/dist/index.d.mts +131 -19
- package/dist/index.d.ts +131 -19
- package/dist/index.js +2 -2
- package/dist/index.mjs +2 -2
- package/package.json +5 -3
- package/tools/reader-meta-text.d.mts +2 -0
- package/tools/reader-meta-text.mjs +26 -0
- package/tools/strip-reader-meta.d.mts +33 -0
- package/tools/strip-reader-meta.mjs +131 -0
package/dist/analysis/client.mjs
CHANGED
|
@@ -4,7 +4,8 @@ import {
|
|
|
4
4
|
KeyRegistrationOperationFull
|
|
5
5
|
} from "./utils/key-operation-full.mjs";
|
|
6
6
|
import { reserveCaptureSeq } from "./low-events/capture-sequence.mjs";
|
|
7
|
-
import {
|
|
7
|
+
import { readSourcePath } from "./readers/page.mjs";
|
|
8
|
+
import { readUserAgent } from "./readers/device.mjs";
|
|
8
9
|
import { SharedWindowObserver, getCapturePageId } from "./low-events/shared-window-observer.mjs";
|
|
9
10
|
import {
|
|
10
11
|
AuthEventName
|
|
@@ -33,7 +34,7 @@ import { PasskeyLoginOperationFull } from "./utils/passkey-login-operation-full.
|
|
|
33
34
|
import { EmailOtpOperationFull } from "./utils/email-otp-operation-full.mjs";
|
|
34
35
|
import { SmsOtpOperationFull } from "./utils/sms-otp-operation-full.mjs";
|
|
35
36
|
import { isBlockedUserAgent } from "./utils/blocked-user-agents.mjs";
|
|
36
|
-
import { withWebLock } from "./
|
|
37
|
+
import { hasWebLocks, withWebLock } from "./readers/web-lock.mjs";
|
|
37
38
|
import { SDK_NAME, SDK_VERSION } from "./version.mjs";
|
|
38
39
|
import {
|
|
39
40
|
EmailLinkContinuationOperation,
|
|
@@ -507,7 +508,23 @@ var CorbadoTracker = class _CorbadoTracker {
|
|
|
507
508
|
if (typeof navigator === "undefined") {
|
|
508
509
|
return false;
|
|
509
510
|
}
|
|
510
|
-
return isBlockedUserAgent(
|
|
511
|
+
return isBlockedUserAgent(
|
|
512
|
+
readUserAgent({
|
|
513
|
+
key: "browser-api:sdk.blocked-user-agent",
|
|
514
|
+
title: "User agent check for crawlers and bots",
|
|
515
|
+
accessNote: "navigator.userAgent, matched against a list of crawler and bot patterns",
|
|
516
|
+
group: "Observe SDK state",
|
|
517
|
+
origin: "observe-external",
|
|
518
|
+
fields: [
|
|
519
|
+
{
|
|
520
|
+
path: "result",
|
|
521
|
+
label: "Whether the browser looks like a bot",
|
|
522
|
+
exposure: "computed",
|
|
523
|
+
transform: { note: "matched against the blocked user-agent list; only the boolean is used (tracking off)" }
|
|
524
|
+
}
|
|
525
|
+
]
|
|
526
|
+
})
|
|
527
|
+
);
|
|
511
528
|
}
|
|
512
529
|
resolveSessionId() {
|
|
513
530
|
try {
|
|
@@ -678,27 +695,74 @@ var CorbadoTracker = class _CorbadoTracker {
|
|
|
678
695
|
return current;
|
|
679
696
|
};
|
|
680
697
|
const timeoutMs = this.config.seqLockTimeoutMs;
|
|
681
|
-
if (timeoutMs === 0 ||
|
|
698
|
+
if (timeoutMs === 0 || !hasWebLocks({
|
|
699
|
+
key: "browser-api:sdk.web-locks",
|
|
700
|
+
title: "Coordination of Observe data across open tabs",
|
|
701
|
+
accessNote: "Web Locks API (navigator.locks) on Observe's own lock names",
|
|
702
|
+
group: "Observe SDK state",
|
|
703
|
+
origin: "observe-internal",
|
|
704
|
+
fields: [
|
|
705
|
+
{
|
|
706
|
+
path: "available",
|
|
707
|
+
label: "Whether the browser supports tab coordination",
|
|
708
|
+
exposure: "enum",
|
|
709
|
+
transform: { values: [true, false] }
|
|
710
|
+
}
|
|
711
|
+
]
|
|
712
|
+
})) {
|
|
682
713
|
return allocate();
|
|
683
714
|
}
|
|
684
|
-
return withWebLock(
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
715
|
+
return withWebLock(
|
|
716
|
+
{
|
|
717
|
+
key: "browser-api:sdk.web-locks",
|
|
718
|
+
title: "Coordination of Observe data across open tabs",
|
|
719
|
+
accessNote: "Web Locks API (navigator.locks) on Observe's own lock names",
|
|
720
|
+
group: "Observe SDK state",
|
|
721
|
+
origin: "observe-internal",
|
|
722
|
+
fields: [
|
|
723
|
+
{
|
|
724
|
+
path: "result",
|
|
725
|
+
label: "Coordination between open tabs",
|
|
726
|
+
exposure: "none",
|
|
727
|
+
transform: { note: "own cbo_* lock names; serializes the SDK's storage updates across tabs" }
|
|
728
|
+
}
|
|
729
|
+
]
|
|
730
|
+
},
|
|
731
|
+
this.seqLockName,
|
|
732
|
+
allocate,
|
|
733
|
+
{
|
|
734
|
+
timeoutMs,
|
|
735
|
+
onTimeout: () => {
|
|
736
|
+
this.logger.warn(`Sequence Web Lock acquisition timed out after ${timeoutMs}ms; using fallback`);
|
|
737
|
+
this.reportTelemetry("info", `seq web lock timeout; using fallback, ${timeoutMs}ms`);
|
|
738
|
+
}
|
|
689
739
|
}
|
|
690
|
-
|
|
740
|
+
);
|
|
691
741
|
}
|
|
692
742
|
resolveTrackingSourcePath() {
|
|
693
743
|
if (typeof window === "undefined") {
|
|
694
744
|
return void 0;
|
|
695
745
|
}
|
|
696
|
-
|
|
697
|
-
|
|
746
|
+
return readSourcePath(
|
|
747
|
+
{
|
|
748
|
+
key: "page:location.source-path",
|
|
749
|
+
title: "Page path and allowed query parameters",
|
|
750
|
+
accessNote: "location.pathname and location.search; the query is reduced by the sourcePathQuery policy inside the reader (trackingSourcePath)",
|
|
751
|
+
group: "Page",
|
|
752
|
+
origin: "observe-external",
|
|
753
|
+
fields: [
|
|
754
|
+
{ path: "url.path", label: "Page path", exposure: "raw" },
|
|
755
|
+
{
|
|
756
|
+
path: "url.query",
|
|
757
|
+
label: "Page address parameters (allowed ones only)",
|
|
758
|
+
exposure: "redacted",
|
|
759
|
+
transform: { note: "sourcePathQuery 'allowlist': values of non-allowlisted keys become '*'; 'all': raw" }
|
|
760
|
+
}
|
|
761
|
+
]
|
|
762
|
+
},
|
|
698
763
|
this.config.sourcePathQuery,
|
|
699
764
|
this.config.sourcePathQueryAllow
|
|
700
765
|
);
|
|
701
|
-
return `${window.location.pathname}${search}`;
|
|
702
766
|
}
|
|
703
767
|
/**
|
|
704
768
|
* Per-tab id for event meta, cached after the first successful resolution: the id is
|
|
@@ -1535,56 +1599,18 @@ var CorbadoTracker = class _CorbadoTracker {
|
|
|
1535
1599
|
smsOtpOperationFull(config) {
|
|
1536
1600
|
return new SmsOtpOperationFull(this, config);
|
|
1537
1601
|
}
|
|
1538
|
-
/**
|
|
1539
|
-
* Track a code from an authenticator app (TOTP) through its verification.
|
|
1540
|
-
*
|
|
1541
|
-
* @param config - Optional code input field(s) and spec type (`totp-login` or `totp-enrollment`).
|
|
1542
|
-
* @remarks
|
|
1543
|
-
* There is no send step: the code comes from the user's authenticator, not from the application.
|
|
1544
|
-
* Report the verification request on postResponse. Bind the code input to capture typing and
|
|
1545
|
-
* autofill; use setInputFields when the input is rendered after the operation starts.
|
|
1546
|
-
* @example
|
|
1547
|
-
* ```ts
|
|
1548
|
-
* const totp = tracker.totpOperationFull({ explicitSpecType: "totp-login", inputHtmlField: codeInput });
|
|
1549
|
-
* totp.postResponse.start({});
|
|
1550
|
-
* totp.postResponse.finished({});
|
|
1551
|
-
* ```
|
|
1552
|
-
*/
|
|
1553
1602
|
totpOperationFull(config) {
|
|
1554
1603
|
return new TotpOperationFull(this, config);
|
|
1555
1604
|
}
|
|
1556
1605
|
provideIdentifierOperationFull(config) {
|
|
1557
|
-
return new OperationFullProvideIdentifierWithCUI(
|
|
1606
|
+
return new OperationFullProvideIdentifierWithCUI(
|
|
1607
|
+
this,
|
|
1608
|
+
config
|
|
1609
|
+
);
|
|
1558
1610
|
}
|
|
1559
1611
|
provideDataOperationFull(config) {
|
|
1560
1612
|
return new ProvideDataOperationFull(this, config);
|
|
1561
1613
|
}
|
|
1562
|
-
/**
|
|
1563
|
-
* Track one form: a page, step or modal where the user provides one coherent set of data and submits
|
|
1564
|
-
* it, typically as one step of a custom process flow such as an onboarding.
|
|
1565
|
-
*
|
|
1566
|
-
* @param config - The form's stable `name` (required); optionally the names of its `fields`, the
|
|
1567
|
-
* `inputHtmlFields` to collect interaction signals from, and `startedAt` when the form became
|
|
1568
|
-
* visible earlier than this call.
|
|
1569
|
-
* @remarks
|
|
1570
|
-
* Create the operation when the form is shown; one operation per visit, so going back to a form
|
|
1571
|
-
* creates a new one with the same name. Report `submit` when the application accepted the form, or
|
|
1572
|
-
* `skip` when the user left it forward without submitting (e.g. "Do it later"). A form that ends with
|
|
1573
|
-
* neither counts as abandoned; one whose last submit failed counts as failed. Without a name the form
|
|
1574
|
-
* is not counted. Send field names only, never values. A form spread over several pages stays one
|
|
1575
|
-
* operation: bind each page's inputs with `setInputFields`.
|
|
1576
|
-
* @example
|
|
1577
|
-
* ```ts
|
|
1578
|
-
* const form = tracker.formOperationFull({ name: "personal-details", fields: ["firstName", "lastName"] });
|
|
1579
|
-
* form.submit.start({});
|
|
1580
|
-
* try {
|
|
1581
|
-
* await savePersonalDetails(values);
|
|
1582
|
-
* form.submit.finished({});
|
|
1583
|
-
* } catch (error) {
|
|
1584
|
-
* form.submit.error(error);
|
|
1585
|
-
* }
|
|
1586
|
-
* ```
|
|
1587
|
-
*/
|
|
1588
1614
|
formOperationFull(config) {
|
|
1589
1615
|
return new FormOperationFull(this, config);
|
|
1590
1616
|
}
|
|
@@ -1829,7 +1855,7 @@ var PROBE_PAGE_ID_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwx
|
|
|
1829
1855
|
function randomProbePageId() {
|
|
1830
1856
|
const bytes = new Uint8Array(16);
|
|
1831
1857
|
try {
|
|
1832
|
-
|
|
1858
|
+
crypto.getRandomValues(bytes);
|
|
1833
1859
|
} catch {
|
|
1834
1860
|
for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
1835
1861
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/config-manager.ts
|
|
2
2
|
import { cacheConfig, loadCachedConfig, parseReliabilityConfig } from "./config.mjs";
|
|
3
|
+
import { sendFetch } from "./readers/network.mjs";
|
|
3
4
|
var CONFIG_FETCH_TIMEOUT_MS = 10 * 1e3;
|
|
4
5
|
var CONFIG_REFRESH_INTERVAL_MS = 10 * 60 * 1e3;
|
|
5
6
|
var CONFIG_RETRY_DELAYS_MS = [1e3, 3e3];
|
|
@@ -36,11 +37,35 @@ var ConfigManager = class {
|
|
|
36
37
|
this.requestTimer = controller ? setTimeout(() => controller.abort(), CONFIG_FETCH_TIMEOUT_MS) : void 0;
|
|
37
38
|
let retryable = true;
|
|
38
39
|
try {
|
|
39
|
-
const response = await
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
const response = await sendFetch(
|
|
41
|
+
{
|
|
42
|
+
key: "network:observe.sdk-config",
|
|
43
|
+
title: "Observe settings download",
|
|
44
|
+
accessNote: "fetch to the Observe API; the response is the SDK reliability config",
|
|
45
|
+
group: "Event delivery",
|
|
46
|
+
origin: "observe-internal",
|
|
47
|
+
fields: [
|
|
48
|
+
{ path: "response.status", label: "Response code", exposure: "raw" },
|
|
49
|
+
{
|
|
50
|
+
path: "response.header.retry-after",
|
|
51
|
+
label: "Whether the server asked to retry later",
|
|
52
|
+
exposure: "presence"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
path: "response.body",
|
|
56
|
+
label: "Observe settings",
|
|
57
|
+
exposure: "raw",
|
|
58
|
+
transform: { note: "SDK reliability config" }
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
this.url,
|
|
63
|
+
{
|
|
64
|
+
method: "GET",
|
|
65
|
+
cache: "no-store",
|
|
66
|
+
...controller ? { signal: controller.signal } : {}
|
|
67
|
+
}
|
|
68
|
+
);
|
|
44
69
|
if (!response.ok) {
|
|
45
70
|
retryable = (response.status === 408 || response.status === 429 || response.status >= 500) && !response.headers.has("Retry-After");
|
|
46
71
|
throw new Error(`SDK config request failed (${response.status})`);
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
// src/device-info-manager.ts
|
|
2
|
+
import { readDocumentHidden } from "./readers/page.mjs";
|
|
2
3
|
function isDocumentHidden() {
|
|
3
|
-
return typeof document !== "undefined" &&
|
|
4
|
+
return typeof document !== "undefined" && readDocumentHidden({
|
|
5
|
+
key: "page:lifecycle.visibility",
|
|
6
|
+
title: "Page visibility",
|
|
7
|
+
accessNote: "document.visibilityState",
|
|
8
|
+
group: "Page lifecycle",
|
|
9
|
+
origin: "observe-external",
|
|
10
|
+
fields: [
|
|
11
|
+
{
|
|
12
|
+
path: "visibilityState",
|
|
13
|
+
label: "Whether the page is hidden",
|
|
14
|
+
exposure: "enum",
|
|
15
|
+
transform: { values: ["hidden", "other"] }
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
});
|
|
4
19
|
}
|
|
5
20
|
var DeviceInfoManager = class {
|
|
6
21
|
constructor(logger, source, refreshInterval, attachInterval, diagnostics, onUndelivered) {
|
package/dist/analysis/device.mjs
CHANGED
|
@@ -180,7 +180,18 @@ var DeviceCollector = class {
|
|
|
180
180
|
"navigator.bluetooth.getAvailability",
|
|
181
181
|
() => readBluetoothAvailability({
|
|
182
182
|
key: "browser-api:device.bluetooth-availability",
|
|
183
|
-
|
|
183
|
+
title: "Bluetooth availability",
|
|
184
|
+
accessNote: "navigator.bluetooth.getAvailability()",
|
|
185
|
+
group: "Device information",
|
|
186
|
+
origin: "observe-external",
|
|
187
|
+
fields: [
|
|
188
|
+
{
|
|
189
|
+
path: "result",
|
|
190
|
+
label: "Whether Bluetooth is available",
|
|
191
|
+
exposure: "enum",
|
|
192
|
+
transform: { values: [true, false] }
|
|
193
|
+
}
|
|
194
|
+
]
|
|
184
195
|
})
|
|
185
196
|
),
|
|
186
197
|
this.startField(
|
|
@@ -188,9 +199,14 @@ var DeviceCollector = class {
|
|
|
188
199
|
"PublicKeyCredential.getClientCapabilities",
|
|
189
200
|
() => readClientCapabilities({
|
|
190
201
|
key: "browser-api:device.client-capabilities",
|
|
202
|
+
title: "Passkey features supported by the browser",
|
|
203
|
+
accessNote: "PublicKeyCredential.getClientCapabilities(), every returned key",
|
|
204
|
+
group: "Device information",
|
|
205
|
+
origin: "observe-external",
|
|
191
206
|
fields: [
|
|
192
207
|
{
|
|
193
208
|
path: "result.<capability>",
|
|
209
|
+
label: "Supported passkey features",
|
|
194
210
|
exposure: "enum",
|
|
195
211
|
transform: { values: ["true", "false"], note: "open set of capability names, kept as reported" }
|
|
196
212
|
}
|
|
@@ -202,7 +218,18 @@ var DeviceCollector = class {
|
|
|
202
218
|
"PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable",
|
|
203
219
|
() => readUserVerifyingPlatformAuthenticatorAvailable({
|
|
204
220
|
key: "browser-api:device.uvpaa",
|
|
205
|
-
|
|
221
|
+
title: "Built-in passkey support on the device",
|
|
222
|
+
accessNote: "PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()",
|
|
223
|
+
group: "Device information",
|
|
224
|
+
origin: "observe-external",
|
|
225
|
+
fields: [
|
|
226
|
+
{
|
|
227
|
+
path: "result",
|
|
228
|
+
label: "Whether the device has a built-in passkey authenticator",
|
|
229
|
+
exposure: "enum",
|
|
230
|
+
transform: { values: [true, false] }
|
|
231
|
+
}
|
|
232
|
+
]
|
|
206
233
|
})
|
|
207
234
|
),
|
|
208
235
|
this.startField(
|
|
@@ -210,7 +237,18 @@ var DeviceCollector = class {
|
|
|
210
237
|
"PublicKeyCredential.isConditionalMediationAvailable",
|
|
211
238
|
() => readConditionalMediationAvailable({
|
|
212
239
|
key: "browser-api:device.conditional-mediation",
|
|
213
|
-
|
|
240
|
+
title: "Passkey autofill availability",
|
|
241
|
+
accessNote: "PublicKeyCredential.isConditionalMediationAvailable()",
|
|
242
|
+
group: "Device information",
|
|
243
|
+
origin: "observe-external",
|
|
244
|
+
fields: [
|
|
245
|
+
{
|
|
246
|
+
path: "result",
|
|
247
|
+
label: "Whether passkey autofill is available",
|
|
248
|
+
exposure: "enum",
|
|
249
|
+
transform: { values: [true, false] }
|
|
250
|
+
}
|
|
251
|
+
]
|
|
214
252
|
})
|
|
215
253
|
),
|
|
216
254
|
this.startField(
|
|
@@ -218,11 +256,20 @@ var DeviceCollector = class {
|
|
|
218
256
|
"navigator.userAgentData.getHighEntropyValues",
|
|
219
257
|
() => readJavaScriptHighEntropy({
|
|
220
258
|
key: "browser-api:device.ua-high-entropy",
|
|
259
|
+
title: "Device platform, version and model reported by the browser",
|
|
260
|
+
accessNote: "User-Agent Client Hints: navigator.userAgentData.getHighEntropyValues()",
|
|
261
|
+
group: "Device information",
|
|
262
|
+
origin: "observe-external",
|
|
221
263
|
fields: [
|
|
222
|
-
{ path: "result.platform", exposure: "raw" },
|
|
223
|
-
{
|
|
224
|
-
|
|
225
|
-
|
|
264
|
+
{ path: "result.platform", label: "Operating system", exposure: "raw" },
|
|
265
|
+
{
|
|
266
|
+
path: "result.mobile",
|
|
267
|
+
label: "Whether the device is a mobile device",
|
|
268
|
+
exposure: "enum",
|
|
269
|
+
transform: { values: [true, false] }
|
|
270
|
+
},
|
|
271
|
+
{ path: "result.platformVersion", label: "Operating system version", exposure: "raw" },
|
|
272
|
+
{ path: "result.model", label: "Device model", exposure: "raw" }
|
|
226
273
|
]
|
|
227
274
|
})
|
|
228
275
|
),
|
|
@@ -231,7 +278,18 @@ var DeviceCollector = class {
|
|
|
231
278
|
"navigator.webdriver",
|
|
232
279
|
() => readWebdriver({
|
|
233
280
|
key: "browser-api:device.webdriver",
|
|
234
|
-
|
|
281
|
+
title: "Browser automation flag",
|
|
282
|
+
accessNote: "navigator.webdriver",
|
|
283
|
+
group: "Device information",
|
|
284
|
+
origin: "observe-external",
|
|
285
|
+
fields: [
|
|
286
|
+
{
|
|
287
|
+
path: "result",
|
|
288
|
+
label: "Whether the browser is controlled by automation",
|
|
289
|
+
exposure: "enum",
|
|
290
|
+
transform: { values: [true, false] }
|
|
291
|
+
}
|
|
292
|
+
]
|
|
235
293
|
})
|
|
236
294
|
),
|
|
237
295
|
this.startField(
|
|
@@ -239,7 +297,11 @@ var DeviceCollector = class {
|
|
|
239
297
|
"navigator.maxTouchPoints",
|
|
240
298
|
() => readMaxTouchPoints({
|
|
241
299
|
key: "browser-api:device.max-touch-points",
|
|
242
|
-
|
|
300
|
+
title: "Touch screen support",
|
|
301
|
+
accessNote: "navigator.maxTouchPoints",
|
|
302
|
+
group: "Device information",
|
|
303
|
+
origin: "observe-external",
|
|
304
|
+
fields: [{ path: "result", label: "Number of touch points of the screen", exposure: "raw" }]
|
|
243
305
|
})
|
|
244
306
|
)
|
|
245
307
|
];
|
|
@@ -377,7 +439,18 @@ var DeviceCollector = class {
|
|
|
377
439
|
source,
|
|
378
440
|
() => typeof __CBO_DATA_PRIVATE_MODE__ === "boolean" && !__CBO_DATA_PRIVATE_MODE__ ? void 0 : readPrivateMode({
|
|
379
441
|
key: "browser-api:device.private-mode",
|
|
380
|
-
|
|
442
|
+
title: "Private browsing mode",
|
|
443
|
+
accessNote: "detectincognitojs library (IndexedDB, storage quota and browser-specific probes)",
|
|
444
|
+
group: "Device information",
|
|
445
|
+
origin: "observe-external",
|
|
446
|
+
fields: [
|
|
447
|
+
{
|
|
448
|
+
path: "result.isPrivate",
|
|
449
|
+
label: "Whether private browsing is on",
|
|
450
|
+
exposure: "enum",
|
|
451
|
+
transform: { values: [true, false] }
|
|
452
|
+
}
|
|
453
|
+
],
|
|
381
454
|
dataConfig: "privateMode"
|
|
382
455
|
})
|
|
383
456
|
);
|
|
@@ -456,9 +529,14 @@ var DeviceCollector = class {
|
|
|
456
529
|
static getUserAgent() {
|
|
457
530
|
return readUserAgent({
|
|
458
531
|
key: "browser-api:device.user-agent",
|
|
532
|
+
title: "Browser and operating system",
|
|
533
|
+
accessNote: "navigator.userAgent",
|
|
534
|
+
group: "Device information",
|
|
535
|
+
origin: "observe-external",
|
|
459
536
|
fields: [
|
|
460
537
|
{
|
|
461
538
|
path: "result",
|
|
539
|
+
label: "Browser and operating system",
|
|
462
540
|
exposure: "class",
|
|
463
541
|
transform: {
|
|
464
542
|
classes: [
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// src/low-events/grouped-input-tracker.ts
|
|
2
2
|
import { InputChangeRecorder } from "./input-change-recorder.mjs";
|
|
3
|
-
import { InputPasteObserver } from "
|
|
3
|
+
import { InputPasteObserver } from "../readers/input-paste.mjs";
|
|
4
|
+
import { observeInputField, readFocusedField, readInputGroupValueLength } from "../readers/input.mjs";
|
|
5
|
+
import { listenWindow } from "../readers/page.mjs";
|
|
4
6
|
var LowEventGroupedInputTracker = class {
|
|
5
7
|
constructor(logger, fields, sink) {
|
|
6
8
|
this.logger = logger;
|
|
@@ -13,6 +15,27 @@ var LowEventGroupedInputTracker = class {
|
|
|
13
15
|
sink.reserveCaptureSeq
|
|
14
16
|
);
|
|
15
17
|
this.paste = new InputPasteObserver(
|
|
18
|
+
{
|
|
19
|
+
key: "dom:input.paste",
|
|
20
|
+
title: "Paste into tracked input fields",
|
|
21
|
+
accessNote: "paste events on the input elements the integration passes to Observe; pasted values never leave the reader",
|
|
22
|
+
group: "Input interaction",
|
|
23
|
+
origin: "observe-external",
|
|
24
|
+
fields: [
|
|
25
|
+
{
|
|
26
|
+
path: "event.paste",
|
|
27
|
+
label: "Paste into the field",
|
|
28
|
+
exposure: "presence",
|
|
29
|
+
transform: { note: "trusted paste events only" }
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
path: "value",
|
|
33
|
+
label: "Length of the pasted text",
|
|
34
|
+
exposure: "size",
|
|
35
|
+
transform: { note: "inserted length only; values are compared in memory and discarded" }
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
},
|
|
16
39
|
this.inputs,
|
|
17
40
|
logger,
|
|
18
41
|
(ts, _inserted, captureSeq) => {
|
|
@@ -32,35 +55,115 @@ var LowEventGroupedInputTracker = class {
|
|
|
32
55
|
);
|
|
33
56
|
this.unregisterInputs = sink.registerInputs?.(this.inputs, () => this.flushPending());
|
|
34
57
|
this.capturePrefill();
|
|
58
|
+
const guard = (_name, run) => this.safe(run);
|
|
35
59
|
for (const input of this.inputs) {
|
|
36
|
-
this.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
60
|
+
this.listeners.push(
|
|
61
|
+
observeInputField(
|
|
62
|
+
{
|
|
63
|
+
key: "dom:input.events",
|
|
64
|
+
title: "Interaction with tracked input fields",
|
|
65
|
+
accessNote: "focus, blur, pointerdown, pointerup, click, input and keyup events on the input elements the integration passes to Observe, reduced to booleans and identities",
|
|
66
|
+
group: "Input interaction",
|
|
67
|
+
origin: "observe-external",
|
|
68
|
+
fields: [
|
|
69
|
+
{ path: "event.focus", label: "Field selected", exposure: "presence" },
|
|
70
|
+
{
|
|
71
|
+
path: "event.focus.relatedTarget",
|
|
72
|
+
label: "Whether focus moved between fields of the same group",
|
|
73
|
+
exposure: "computed",
|
|
74
|
+
transform: { note: "only whether focus moves within the group" }
|
|
75
|
+
},
|
|
76
|
+
{ path: "event.blur", label: "Field left", exposure: "presence" },
|
|
77
|
+
{ path: "event.pointerdown", label: "Pointer pressed on the field", exposure: "presence" },
|
|
78
|
+
{ path: "event.pointerup", label: "Pointer released on the field", exposure: "presence" },
|
|
79
|
+
{ path: "event.click", label: "Click on the field", exposure: "presence" },
|
|
80
|
+
{ path: "event.input", label: "Typing in the field", exposure: "presence" },
|
|
81
|
+
{
|
|
82
|
+
path: "event.input.isComposing",
|
|
83
|
+
label: "Whether text is being composed with an input method",
|
|
84
|
+
exposure: "enum",
|
|
85
|
+
transform: { values: [true, false] }
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
path: "event.keyup.key",
|
|
89
|
+
label: "Escape key pressed",
|
|
90
|
+
exposure: "enum",
|
|
91
|
+
transform: { values: ["Escape", "other"] }
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
input,
|
|
96
|
+
{
|
|
97
|
+
input: (composing, event) => {
|
|
98
|
+
if (this.paste.handlesInput(event)) return;
|
|
99
|
+
this.composing ||= composing;
|
|
100
|
+
if (this.changeTimer !== null) return;
|
|
101
|
+
this.changeTime = Date.now();
|
|
102
|
+
this.changeSeq = sink.reserveCaptureSeq?.();
|
|
103
|
+
this.changeTimer = setTimeout(() => this.safe(() => this.flushChanges()), 0);
|
|
104
|
+
},
|
|
105
|
+
focus: (relatedTarget) => {
|
|
106
|
+
if (this.contains(relatedTarget)) return;
|
|
107
|
+
this.emit("focus");
|
|
108
|
+
},
|
|
109
|
+
blur: (relatedTarget) => {
|
|
110
|
+
if (this.contains(relatedTarget)) return;
|
|
111
|
+
this.flushPending();
|
|
112
|
+
this.emit("blur");
|
|
113
|
+
},
|
|
114
|
+
click: () => this.emit("click"),
|
|
115
|
+
pointerdown: () => this.emit("pointerdown"),
|
|
116
|
+
pointerup: () => this.emit("pointerup"),
|
|
117
|
+
keyupEscape: () => this.emit("keyup-escape")
|
|
118
|
+
},
|
|
119
|
+
guard,
|
|
120
|
+
["input", "focus", "blur", "click", "pointerdown", "pointerup", "keyup"]
|
|
121
|
+
)
|
|
122
|
+
);
|
|
57
123
|
}
|
|
58
124
|
if (typeof window !== "undefined")
|
|
59
|
-
this.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
125
|
+
this.listeners.push(
|
|
126
|
+
listenWindow(
|
|
127
|
+
{
|
|
128
|
+
key: "page:lifecycle.pagehide",
|
|
129
|
+
title: "Leaving the page",
|
|
130
|
+
accessNote: "pagehide event on window",
|
|
131
|
+
group: "Page lifecycle",
|
|
132
|
+
origin: "observe-external",
|
|
133
|
+
fields: [
|
|
134
|
+
{
|
|
135
|
+
path: "event.pagehide",
|
|
136
|
+
label: "Leaving the page",
|
|
137
|
+
exposure: "presence",
|
|
138
|
+
transform: { note: "flushes pending input low events on unload" }
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
},
|
|
142
|
+
"pagehide",
|
|
143
|
+
(_event) => this.safe(() => {
|
|
144
|
+
this.flushPending();
|
|
145
|
+
sink.flushKeepalive();
|
|
146
|
+
})
|
|
147
|
+
)
|
|
148
|
+
);
|
|
149
|
+
if (typeof document !== "undefined" && readFocusedField(
|
|
150
|
+
{
|
|
151
|
+
key: "dom:input.focused",
|
|
152
|
+
title: "Whether a tracked input field has focus",
|
|
153
|
+
accessNote: "document.activeElement compared with the passed input elements",
|
|
154
|
+
group: "Input interaction",
|
|
155
|
+
origin: "observe-external",
|
|
156
|
+
fields: [
|
|
157
|
+
{
|
|
158
|
+
path: "focused",
|
|
159
|
+
label: "Whether the field is selected",
|
|
160
|
+
exposure: "enum",
|
|
161
|
+
transform: { values: [true, false] }
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
},
|
|
165
|
+
this.inputs
|
|
166
|
+
)) {
|
|
64
167
|
this.emit("focus");
|
|
65
168
|
}
|
|
66
169
|
}
|
|
@@ -78,7 +181,24 @@ var LowEventGroupedInputTracker = class {
|
|
|
78
181
|
return this.inputs.some((field) => field === target);
|
|
79
182
|
}
|
|
80
183
|
length() {
|
|
81
|
-
return
|
|
184
|
+
return readInputGroupValueLength(
|
|
185
|
+
{
|
|
186
|
+
key: "dom:input.content",
|
|
187
|
+
title: "Length of the text in tracked input fields",
|
|
188
|
+
accessNote: "input value length only; the value itself does not leave the reader",
|
|
189
|
+
group: "Input interaction",
|
|
190
|
+
origin: "observe-external",
|
|
191
|
+
fields: [
|
|
192
|
+
{
|
|
193
|
+
path: "value.length",
|
|
194
|
+
label: "Length of the entered text",
|
|
195
|
+
exposure: "size",
|
|
196
|
+
transform: { note: "summed over the fields of a group" }
|
|
197
|
+
}
|
|
198
|
+
]
|
|
199
|
+
},
|
|
200
|
+
this.inputs
|
|
201
|
+
);
|
|
82
202
|
}
|
|
83
203
|
safe(action) {
|
|
84
204
|
try {
|
|
@@ -87,11 +207,6 @@ var LowEventGroupedInputTracker = class {
|
|
|
87
207
|
this.logger.error("Failed to observe grouped input", error);
|
|
88
208
|
}
|
|
89
209
|
}
|
|
90
|
-
listen(target, type, handler) {
|
|
91
|
-
const wrapped = (event) => this.safe(() => handler(event));
|
|
92
|
-
target.addEventListener(type, wrapped);
|
|
93
|
-
this.listeners.push(() => target.removeEventListener(type, wrapped));
|
|
94
|
-
}
|
|
95
210
|
cancelChange() {
|
|
96
211
|
if (this.changeTimer !== null) clearTimeout(this.changeTimer);
|
|
97
212
|
this.changeTimer = null;
|