@levo-so/insights 0.3.10 → 0.3.11
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/constants/storageKeys.d.ts +22 -17
- package/dist/index.d.ts +2 -0
- package/dist/index.js +181 -379
- package/dist/lib/identityHeaders.d.ts +22 -0
- package/dist/lib/insights.d.ts +2 -1
- package/dist/lib/rotation.d.ts +42 -0
- package/dist/lib/transport.d.ts +45 -0
- package/dist/stores/events.d.ts +12 -17
- package/dist/stores/identity.d.ts +111 -0
- package/dist/stores/readiness.d.ts +6 -6
- package/dist/stores/session.d.ts +12 -21
- package/dist/stores/sessionProperties.d.ts +116 -0
- package/dist/stores.d.ts +1 -0
- package/dist/stores.js +2 -2
- package/dist/textSelection-CXG4e3wj.js +948 -0
- package/dist/types/endpoint.d.ts +38 -0
- package/dist/types/identity.d.ts +0 -1
- package/dist/utils/cookies.d.ts +23 -0
- package/dist/utils/detectMode.d.ts +20 -7
- package/dist/utils/getAsyncUserProperties.d.ts +6 -0
- package/dist/utils/getAuthHeaders.d.ts +17 -4
- package/dist/utils/getUserProperties.d.ts +28 -3
- package/dist/utils/sanitizeEvent.d.ts +51 -0
- package/package.json +9 -6
- package/dist/textSelection-Cwq6lqKg.js +0 -604
- package/dist/utils/pingEndpoint.d.ts +0 -4
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where events go, and whether that has been proven.
|
|
3
|
+
*
|
|
4
|
+
* A union rather than three nullable urls on the session. The three states are
|
|
5
|
+
* mutually exclusive, and the old shape (`insightsBaseUrl` / `candidateBaseUrl`
|
|
6
|
+
* / `fallbackBaseUrl`) let every reader re-derive which one it was in, and
|
|
7
|
+
* made "insightsBaseUrl is set" a comment-enforced invariant that `resolveApi`
|
|
8
|
+
* in the SDK app depended on. Here the invariant is the type.
|
|
9
|
+
*/
|
|
10
|
+
type IEndpoint =
|
|
11
|
+
/** Nothing usable was configured. Events queue and are never sent. */
|
|
12
|
+
{
|
|
13
|
+
status: "none";
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A proxy url that nothing has confirmed reaches Levo yet. The first event is
|
|
17
|
+
* the probe: its response either promotes this to `proven` or falls back.
|
|
18
|
+
* Always proxy, a direct host needs no proving.
|
|
19
|
+
*/
|
|
20
|
+
| {
|
|
21
|
+
status: "unproven";
|
|
22
|
+
url: string;
|
|
23
|
+
fallback: string;
|
|
24
|
+
}
|
|
25
|
+
/** Confirmed: a request to this url came back as ours. */
|
|
26
|
+
| {
|
|
27
|
+
status: "proven";
|
|
28
|
+
url: string;
|
|
29
|
+
mode: "proxy" | "direct";
|
|
30
|
+
};
|
|
31
|
+
/** The url to send to, or null when there is nowhere to send. */
|
|
32
|
+
declare const endpointUrl: (endpoint: IEndpoint) => string | null;
|
|
33
|
+
/**
|
|
34
|
+
* How to authenticate to this endpoint. An unproven candidate is always proxy,
|
|
35
|
+
* where the browser attaches the identity cookies on a same-origin request.
|
|
36
|
+
*/
|
|
37
|
+
declare const endpointMode: (endpoint: IEndpoint) => "proxy" | "direct";
|
|
38
|
+
export { type IEndpoint, endpointMode, endpointUrl, };
|
package/dist/types/identity.d.ts
CHANGED
|
@@ -6,7 +6,6 @@ export interface IIdentity {
|
|
|
6
6
|
locale: ILevoAudience.Traits["locale"] | null;
|
|
7
7
|
timezone: ILevoAudience.Traits["timezone"] | null;
|
|
8
8
|
darkMode: ILevoAudience.Traits["dark_mode"] | null;
|
|
9
|
-
privateMode: ILevoAudience.Traits["private_mode"] | null;
|
|
10
9
|
referrer: IIdentityReferrer | null;
|
|
11
10
|
properties: ILevoAudience.Traits["properties"] | null;
|
|
12
11
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-party cookie helpers.
|
|
3
|
+
*
|
|
4
|
+
* Cookies are the only client store that is shared across tabs *and* readable by
|
|
5
|
+
* the server on the same request, which is why identity mirrors into them: the
|
|
6
|
+
* proxy-mode backend resolves `id_i_cd`/`id_i_cs` straight off the `Cookie`
|
|
7
|
+
* header without the SDK having to attach anything.
|
|
8
|
+
*
|
|
9
|
+
* All writes are host-only (no `Domain` attribute) and `path=/`, matching the
|
|
10
|
+
* scope the server uses for its own `id_i_d`/`id_i_s` cookies.
|
|
11
|
+
*/
|
|
12
|
+
/** Read a cookie by name. Returns null when absent, blocked, or empty. */
|
|
13
|
+
declare const readCookie: (name: string) => string | null;
|
|
14
|
+
/**
|
|
15
|
+
* Write a host-only, path=/ cookie.
|
|
16
|
+
*
|
|
17
|
+
* `SameSite=Lax` (not `Strict`) so identity survives a cross-site entry, an ad
|
|
18
|
+
* or social click landing on the site is exactly the visit attribution cares
|
|
19
|
+
* about. `Secure` only on https, because localhost testbeds run over http and a
|
|
20
|
+
* Secure cookie there is silently dropped.
|
|
21
|
+
*/
|
|
22
|
+
declare const writeCookie: (name: string, value: string, maxAgeSeconds: number) => void;
|
|
23
|
+
export { readCookie, writeCookie, };
|
|
@@ -1,10 +1,23 @@
|
|
|
1
|
+
import { IEndpoint } from '../types/endpoint';
|
|
1
2
|
import { IInsightOptions } from '../types/options';
|
|
2
|
-
type DetectionResult = {
|
|
3
|
-
mode: "proxy" | "direct";
|
|
4
|
-
url: string | null;
|
|
5
|
-
};
|
|
6
3
|
/**
|
|
7
|
-
*
|
|
4
|
+
* Decide where events should go, without asking the network first.
|
|
5
|
+
*
|
|
6
|
+
* This used to race a `GET /ping` against both endpoints and wait up to a
|
|
7
|
+
* second for the answer, and events queued for the whole of it. That cost the
|
|
8
|
+
* first-ever page load of every visitor a round trip *before* anything could
|
|
9
|
+
* be sent, which is the exact window a bouncing visitor leaves in, the ping
|
|
10
|
+
* had replaced `/welcome` as the thing standing between a quick exit and any
|
|
11
|
+
* record of it.
|
|
12
|
+
*
|
|
13
|
+
* The first event is now the probe. If the proxy is not really forwarding,
|
|
14
|
+
* that request fails on its own. Usually faster than a ping would have
|
|
15
|
+
* answered, because a missing route fails at the customer's edge, and the
|
|
16
|
+
* transport retries it against the direct host. A working proxy costs nothing
|
|
17
|
+
* at all: the event that proves it is an event that had to be sent anyway.
|
|
18
|
+
*
|
|
19
|
+
* Only "auto" with both urls configured needs proving. Anything else is
|
|
20
|
+
* already certain, and comes back `proven`.
|
|
8
21
|
*/
|
|
9
|
-
|
|
10
|
-
export {};
|
|
22
|
+
declare const detectMode: (options: IInsightOptions) => IEndpoint;
|
|
23
|
+
export { detectMode, };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
declare const getAsyncUserProperties: () => Promise<{
|
|
2
|
+
private_mode: boolean | undefined;
|
|
3
|
+
properties: Record<string, unknown>;
|
|
4
|
+
} | null>;
|
|
5
|
+
type IAsyncUserProperties = NonNullable<Awaited<ReturnType<typeof getAsyncUserProperties>>>;
|
|
6
|
+
export { type IAsyncUserProperties, getAsyncUserProperties, };
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Format identity headers for one request in a known mode. Returns an empty
|
|
3
|
+
* object in proxy mode: the id_i_cd/id_i_cs cookies ride along automatically
|
|
4
|
+
* on the same-origin request, so no explicit header is needed there.
|
|
5
|
+
*
|
|
6
|
+
* Most callers want {@link getIdentityHeaders} in lib/identityHeaders.ts
|
|
7
|
+
* instead, which reads the SDK's current state and calls this. The transport
|
|
8
|
+
* is the exception: it formats the ids it captured at flush time for the mode
|
|
9
|
+
* of the attempt in hand, never live state.
|
|
10
|
+
*
|
|
11
|
+
* deviceToken (legacy direct-mode device JWT, pre-adoption) is sent alongside
|
|
12
|
+
* the client mint, not instead of it: the server ranks the legacy credential
|
|
13
|
+
* above the client one and echoes back whichever wins, exactly like proxy
|
|
14
|
+
* mode's automatic id_i_d/id_i_cd dual-attach. There is no session-JWT
|
|
15
|
+
* equivalent. Rotation is fully client-owned, sessions are never adopted.
|
|
4
16
|
*/
|
|
5
|
-
export declare const getAuthHeaders: ({
|
|
17
|
+
export declare const getAuthHeaders: ({ deviceId, sessionId, deviceToken, mode, }: {
|
|
18
|
+
deviceId?: string | null;
|
|
19
|
+
sessionId?: string | null;
|
|
6
20
|
deviceToken?: string | null;
|
|
7
|
-
sessionToken?: string | null;
|
|
8
21
|
mode?: string | null;
|
|
9
22
|
}) => Record<string, string>;
|
|
@@ -1,7 +1,32 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Everything the browser can answer *now*.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately synchronous. This used to await three questions before anything
|
|
5
|
+
* else ran. Private-window detection, battery, and a camera/mic device
|
|
6
|
+
* enumeration, and only then read the values that were already sitting there.
|
|
7
|
+
* `enumerateDevices()` alone measured **154ms**, so the referrer, locale,
|
|
8
|
+
* timezone and screen size that decide a visit's attribution were held behind a
|
|
9
|
+
* check for whether the device has a webcam. Since the first event is what a
|
|
10
|
+
* bouncing visitor's whole visit depends on, that delay was the cost of the
|
|
11
|
+
* fields it was waiting for.
|
|
12
|
+
*
|
|
13
|
+
* Those probes live in getAsyncUserProperties() now, staged when they resolve
|
|
14
|
+
* and merged onto the same session row by the server, so nothing is lost
|
|
15
|
+
* except for a visitor who leaves inside the first few hundred milliseconds.
|
|
16
|
+
*
|
|
17
|
+
* The GPU string went with them despite being synchronous: creating a WebGL
|
|
18
|
+
* context to read it measured **10.8ms**, the most expensive single thing init
|
|
19
|
+
* did, and on a low-end phone that lands next to first paint. Nothing on the
|
|
20
|
+
* first request depends on it.
|
|
21
|
+
*
|
|
22
|
+
* What is left is only what is already sitting in memory, locale, timezone,
|
|
23
|
+
* colour scheme, screen and viewport, cores, memory, network type, all of it
|
|
24
|
+
* free to read, and all of it either attribution or the frame the visit
|
|
25
|
+
* happened in.
|
|
26
|
+
*/
|
|
27
|
+
export declare const getUserProperties: () => {
|
|
2
28
|
locale: string;
|
|
3
29
|
timezone: string;
|
|
4
30
|
dark_mode: boolean;
|
|
5
|
-
private_mode: boolean;
|
|
6
31
|
properties: Record<string, any>;
|
|
7
|
-
}
|
|
32
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { ILevoAudience } from '../types/analytics';
|
|
2
|
+
import { IIdentityReferrer } from '../types/identity';
|
|
3
|
+
/** The server's WorkspaceIdSchema: exactly 8 uppercase alphanumerics. */
|
|
4
|
+
declare const SAFE_WORKSPACE: RegExp;
|
|
5
|
+
/**
|
|
6
|
+
* Whether a url can go on the wire. Mirrors the server's SafeUrlSchema:
|
|
7
|
+
* parseable, ≤2048, and not a javascript:/data:/vbscript: protocol.
|
|
8
|
+
*/
|
|
9
|
+
declare const isSendableUrl: (value: string | undefined | null) => value is string;
|
|
10
|
+
/**
|
|
11
|
+
* Coerce the stored referrer into something the server's schema accepts.
|
|
12
|
+
* Empty hostname is valid (direct traffic); a malformed one is blanked rather
|
|
13
|
+
* than allowed to fail the batch.
|
|
14
|
+
*/
|
|
15
|
+
declare const sanitizeReferrer: (referrer: IIdentityReferrer | null | undefined) => IIdentityReferrer | undefined;
|
|
16
|
+
/** Clamp the session-scoped traits. Mirrors SessionTraitsSchema. */
|
|
17
|
+
declare const sanitizeTraits: (traits: {
|
|
18
|
+
created_at: string;
|
|
19
|
+
darkMode: boolean | null | undefined;
|
|
20
|
+
landingUrl: string | null | undefined;
|
|
21
|
+
locale: string | null | undefined;
|
|
22
|
+
properties: ILevoAudience.Traits["properties"] | null | undefined;
|
|
23
|
+
referrer: IIdentityReferrer | null | undefined;
|
|
24
|
+
timezone: string | null | undefined;
|
|
25
|
+
}) => ILevoAudience.SessionTraits;
|
|
26
|
+
/**
|
|
27
|
+
* An event that has been through {@link sanitizeEvent}.
|
|
28
|
+
*
|
|
29
|
+
* A separate type rather than a cast back to the queued shape: sanitizing
|
|
30
|
+
* genuinely produces something different, because a field that fails the
|
|
31
|
+
* server's rules is DROPPED. Those fields are optional server-side, so the
|
|
32
|
+
* honest wire contract has them optional here too.
|
|
33
|
+
*/
|
|
34
|
+
type ISanitizedEvent<T> = Omit<T, "hostname" | "identifier" | "page_title" | "pathname" | "resource" | "url"> & {
|
|
35
|
+
hostname?: string;
|
|
36
|
+
identifier?: string;
|
|
37
|
+
page_title?: string;
|
|
38
|
+
pathname?: string;
|
|
39
|
+
resource?: string;
|
|
40
|
+
url?: string;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Bring one queued event inside the server's own rules before it goes out.
|
|
44
|
+
*
|
|
45
|
+
* Dropped vs truncated is decided per field by what a wrong value costs. A url
|
|
46
|
+
* or hostname is an identity. Half of one attributes the event to a page or
|
|
47
|
+
* host nobody visited, so a failing value is omitted. A title or a path is
|
|
48
|
+
* descriptive, so truncating keeps most of the meaning.
|
|
49
|
+
*/
|
|
50
|
+
declare const sanitizeEvent: <T extends Record<string, unknown>>(event: T) => ISanitizedEvent<T>;
|
|
51
|
+
export { type ISanitizedEvent, SAFE_WORKSPACE, isSendableUrl, sanitizeEvent, sanitizeReferrer, sanitizeTraits, };
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@levo-so/insights",
|
|
3
3
|
"description": "Levo analytics",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.11",
|
|
5
5
|
"author": "Levo Engineering <devs@theinternetfolks.com>",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@analytics/activity-utils": "0.2.2",
|
|
8
8
|
"@analytics/visitor-source": "0.0.7",
|
|
9
|
+
"@theinternetfolks/snowflake": "1.3.0",
|
|
9
10
|
"analytics": "0.8.14",
|
|
10
11
|
"get-user-locale": "2.3.2",
|
|
11
12
|
"lodash-es": "4.18.1",
|
|
@@ -16,8 +17,8 @@
|
|
|
16
17
|
"typescript": "5.9.3",
|
|
17
18
|
"vite": "8.0.8",
|
|
18
19
|
"vite-plugin-dts": "4.5.4",
|
|
19
|
-
"@levo/
|
|
20
|
-
"@levo-
|
|
20
|
+
"@levo-so/core": "0.3.11",
|
|
21
|
+
"@levo/ts-config": "0.0.0"
|
|
21
22
|
},
|
|
22
23
|
"exports": {
|
|
23
24
|
".": {
|
|
@@ -37,7 +38,7 @@
|
|
|
37
38
|
"main": "./dist/index.js",
|
|
38
39
|
"module": "./dist/index.js",
|
|
39
40
|
"peerDependencies": {
|
|
40
|
-
"@levo-so/core": "0.3.
|
|
41
|
+
"@levo-so/core": "0.3.11"
|
|
41
42
|
},
|
|
42
43
|
"publishConfig": {
|
|
43
44
|
"access": "public"
|
|
@@ -47,9 +48,11 @@
|
|
|
47
48
|
"types": "./dist/index.d.ts",
|
|
48
49
|
"scripts": {
|
|
49
50
|
"build": "vite build",
|
|
50
|
-
"check-types": "tsc --noEmit",
|
|
51
|
+
"check-types": "tsc --noEmit && tsc --noEmit -p tsconfig.demo.json",
|
|
51
52
|
"clean": "npx rimraf dist node_modules .turbo",
|
|
53
|
+
"demo": "vite --config vite.demo.config.ts",
|
|
52
54
|
"dev": "vite build --watch",
|
|
53
|
-
"lib-dev": "vite"
|
|
55
|
+
"lib-dev": "vite --config vite.demo.config.ts",
|
|
56
|
+
"test": "vite build && node --experimental-strip-types --test src/**/*.check.ts"
|
|
54
57
|
}
|
|
55
58
|
}
|