@journeeze/sdk 0.0.0-stage → 0.1.0-rc.2
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/AGENTS.md +67 -0
- package/CHANGELOG.md +30 -0
- package/LICENSE.md +27 -0
- package/README.md +164 -2
- package/dist/THIRD_PARTY_LICENSES.txt +246 -0
- package/dist/consent-CoKD33Rm.js +170 -0
- package/dist/context-UnG6_X61.js +17 -0
- package/dist/contract.js +10 -0
- package/dist/feedback-C4CYtS2A.js +196 -0
- package/dist/help-C1x-y2f9.js +878 -0
- package/dist/hook-BdG4UL4k.js +91 -0
- package/dist/index-B_nob8S0.js +2091 -0
- package/dist/index-CfCbJJXa.js +20 -0
- package/dist/index.js +10 -0
- package/dist/loader-Ba4X-WNl.js +95 -0
- package/dist/react.js +70 -0
- package/dist/resolve-BfWByuTp.js +97 -0
- package/dist/script.js +13 -0
- package/dist/signals-E3VEdmWH.js +477 -0
- package/dist/telemetry-Cik-cYzw.js +140 -0
- package/dist/transport-B5YJFQEF.js +60 -0
- package/dist/types/config.d.ts +17 -0
- package/dist/types/context.d.ts +6 -0
- package/dist/types/contract.d.ts +13 -0
- package/dist/types/help/core/client.d.ts +26 -0
- package/dist/types/help/core/controller.d.ts +175 -0
- package/dist/types/help/core/events.d.ts +8 -0
- package/dist/types/help/core/help.d.ts +24 -0
- package/dist/types/help/core/highlight.d.ts +38 -0
- package/dist/types/help/core/panel.d.ts +11 -0
- package/dist/types/help/core/search.d.ts +7 -0
- package/dist/types/help/ui/callout.d.ts +2 -0
- package/dist/types/help/ui/panel.d.ts +3 -0
- package/dist/types/help/ui/request.d.ts +2 -0
- package/dist/types/help/ui/styles.d.ts +1 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/invite.d.ts +4 -0
- package/dist/types/loader.d.ts +57 -0
- package/dist/types/mask.d.ts +10 -0
- package/dist/types/react.d.ts +9 -0
- package/dist/types/resolve/resolve.d.ts +12 -0
- package/dist/types/script.d.ts +6 -0
- package/dist/types/signals/cel.d.ts +8 -0
- package/dist/types/signals/hook.d.ts +10 -0
- package/dist/types/signals/signals.conformance.d.ts +2 -0
- package/dist/types/signals/signals.d.ts +145 -0
- package/dist/types/telemetry/telemetry.d.ts +63 -0
- package/dist/types/transport.d.ts +30 -0
- package/dist/types/types.d.ts +391 -0
- package/dist/types/widget/anchor.d.ts +11 -0
- package/dist/types/widget/capture.d.ts +28 -0
- package/dist/types/widget/consent.d.ts +25 -0
- package/dist/types/widget/feedback.d.ts +36 -0
- package/dist/types/widget/icons.d.ts +14 -0
- package/dist/types/widget/outbox.d.ts +69 -0
- package/dist/types/widget/styles.d.ts +1 -0
- package/dist/types/widget.d.ts +27 -0
- package/dist/types/wire.d.ts +66 -0
- package/dist/widget-DQZm8sAC.js +1599 -0
- package/package.json +68 -4
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { CheckinRequest, HelpState } from "../help/core/controller";
|
|
2
|
+
/** One event of the closed plan, as the collector's `HelpEngagementEvent` (Connect JSON names). */
|
|
3
|
+
export interface WireHelpEvent {
|
|
4
|
+
event: string;
|
|
5
|
+
journeyId?: string;
|
|
6
|
+
stepIndex?: number;
|
|
7
|
+
entry?: string;
|
|
8
|
+
answer?: string;
|
|
9
|
+
guideVersion?: string;
|
|
10
|
+
/** C0b §7: random, unchanged across retries; the collector counts it once per site per hour. */
|
|
11
|
+
eventId?: string;
|
|
12
|
+
/** C0b §7: 1, 2, 3… within this page load; never stored, never linked across loads. */
|
|
13
|
+
seq?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface RecordHelpEventsRequest {
|
|
16
|
+
siteKey: string;
|
|
17
|
+
consentGiven: true;
|
|
18
|
+
events: WireHelpEvent[];
|
|
19
|
+
/** C0b v1.1 §4: events this page load dropped at an earlier page end (aggregate, no ids). */
|
|
20
|
+
droppedAtUnload?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface TelemetryOptions {
|
|
23
|
+
endpoint: string;
|
|
24
|
+
siteKey: string;
|
|
25
|
+
/** The help controller: events are derived from its state changes. */
|
|
26
|
+
subscribe(listener: (state: HelpState) => void): () => void;
|
|
27
|
+
fetchImpl?: typeof fetch;
|
|
28
|
+
/** Wait before sending a partial batch (1–10 s; default {@link FLUSH_MS}). */
|
|
29
|
+
flushMs?: number;
|
|
30
|
+
/** Randomness for the retry jitter (tests). */
|
|
31
|
+
random?: () => number;
|
|
32
|
+
/** A new event id (tests); default `crypto.randomUUID()` (UUIDv4). */
|
|
33
|
+
newId?: () => string;
|
|
34
|
+
/**
|
|
35
|
+
* Honour Global Privacy Control (default: yes). With GPC on, telemetry is off without asking
|
|
36
|
+
* (C0a §7, ADR 0013 §3); reading help and explicit callouts are not telemetry and still work.
|
|
37
|
+
* Do Not Track is ignored (C0a §7).
|
|
38
|
+
*/
|
|
39
|
+
respectGlobalPrivacyControl?: boolean;
|
|
40
|
+
}
|
|
41
|
+
/** Whether Global Privacy Control turns telemetry off for these options. */
|
|
42
|
+
export declare function gpcRefusesTelemetry(opts: Pick<TelemetryOptions, "respectGlobalPrivacyControl">, nav?: object | undefined): boolean;
|
|
43
|
+
export interface Telemetry {
|
|
44
|
+
/** A check-in answer the reader sent (help calls this after consent). */
|
|
45
|
+
checkin(req: CheckinRequest): void;
|
|
46
|
+
/**
|
|
47
|
+
* Sends what is queued (page-end transport) and stops; nothing is sent afterwards. With
|
|
48
|
+
* `discard` (a logout, `reset()`) the queue is dropped instead: nothing more about this reader
|
|
49
|
+
* leaves.
|
|
50
|
+
*/
|
|
51
|
+
stop(discard?: boolean): void;
|
|
52
|
+
}
|
|
53
|
+
/** The collector's batch limit, and the per-request byte cap (C0b §4). */
|
|
54
|
+
export declare const MAX_BATCH = 50;
|
|
55
|
+
export declare const MAX_BATCH_BYTES: number;
|
|
56
|
+
/** A partial batch waits at most this long (C0b §4: 5 s, configurable 1–10 s). */
|
|
57
|
+
export declare const FLUSH_MS = 5000;
|
|
58
|
+
export declare const MIN_FLUSH_MS = 1000;
|
|
59
|
+
export declare const MAX_FLUSH_MS = 10000;
|
|
60
|
+
/** The in-memory queue cap (C0b §6): this many events or bytes; the oldest are dropped first. */
|
|
61
|
+
export declare const MAX_QUEUED = 200;
|
|
62
|
+
export declare const MAX_QUEUED_BYTES: number;
|
|
63
|
+
export declare function startTelemetry(opts: TelemetryOptions): Telemetry;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** First retry after 1 s, doubling. */
|
|
2
|
+
export declare const RETRY_BASE_MS = 1000;
|
|
3
|
+
/** Retries after the first attempt; 3 for a request that got no response at all (status 0). */
|
|
4
|
+
export declare const MAX_RETRIES = 5;
|
|
5
|
+
export declare const MAX_RETRIES_NO_RESPONSE = 3;
|
|
6
|
+
/** A batch first sent longer ago than this is dropped, so a retry never outlives the 1 h de-duplication window. */
|
|
7
|
+
export declare const MAX_BATCH_AGE_MS: number;
|
|
8
|
+
/** Statuses worth retrying (0 = no response). Every other `4xx` is final. */
|
|
9
|
+
export declare function isRetryable(status: number): boolean;
|
|
10
|
+
/** Whether attempt `retries + 1` may still happen after a failure with `status`. */
|
|
11
|
+
export declare function mayRetry(retries: number, status: number): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* The wait before retry number `retry` (1-based): `1 s × 2^(retry−1)`, capped at 60 s, ±50 %
|
|
14
|
+
* jitter, and never shorter than the server's `Retry-After` (itself capped at 60 s).
|
|
15
|
+
*/
|
|
16
|
+
export declare function retryDelayMs(retry: number, retryAfter?: number, random?: () => number): number;
|
|
17
|
+
/** The page-end budget shared by every Journeeze batched stream on the page (C0b v1.1 §4). */
|
|
18
|
+
export declare const PAGE_END_BUDGET_BYTES: number;
|
|
19
|
+
/** Bytes left in this page end's shared budget. Streams fill it in priority order (C0b §4). */
|
|
20
|
+
export declare function pageEndRemaining(): number;
|
|
21
|
+
/**
|
|
22
|
+
* Sends one batch at page end (C0b v1.1 §4): a `keepalive` fetch (`text/plain`, no credentials,
|
|
23
|
+
* no other header), or one `sendBeacon` where keepalive is unsupported, never both. The body
|
|
24
|
+
* must fit {@link pageEndRemaining}; it is charged to the shared budget. Returns whether the
|
|
25
|
+
* browser accepted it; on `false` the caller keeps the batch for when the page returns and MUST
|
|
26
|
+
* NOT try another keepalive request.
|
|
27
|
+
*/
|
|
28
|
+
export declare function sendAtPageEnd(endpoint: string, method: string, body: unknown, fetchImpl?: typeof fetch): boolean;
|
|
29
|
+
/** Byte length of `value` as JSON (UTF-8). */
|
|
30
|
+
export declare function jsonBytes(value: unknown): number;
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/** Host-declared context: who is reviewing and where they are in a journey. Ids, never user data. */
|
|
2
|
+
export interface JourneezeContext {
|
|
3
|
+
persona?: string | undefined;
|
|
4
|
+
journeyId?: string | undefined;
|
|
5
|
+
journeyStep?: string | undefined;
|
|
6
|
+
/** desktop / tablet / mobile. The widget derives it from the viewport when unset. */
|
|
7
|
+
device?: string | undefined;
|
|
8
|
+
/** Review-item coordinate, e.g. `plumber` (wire field lands with collector lane L3). */
|
|
9
|
+
businessType?: string | undefined;
|
|
10
|
+
/** Review-item coordinate, e.g. `empty` (wire field lands with collector lane L3). */
|
|
11
|
+
state?: string | undefined;
|
|
12
|
+
}
|
|
13
|
+
export interface JourneezeOptions {
|
|
14
|
+
/** Public site key from the Journeeze project settings. Not a secret. */
|
|
15
|
+
siteKey: string;
|
|
16
|
+
/**
|
|
17
|
+
* Where the collector paths are served. Defaults to the host page's own origin: the site proxies
|
|
18
|
+
* `/journeeze.collector.v1.*` to Journeeze (docs/collector-proxy.md), so there is no CORS. A
|
|
19
|
+
* relative (`/jz`) or absolute override is allowed.
|
|
20
|
+
*/
|
|
21
|
+
endpoint?: string;
|
|
22
|
+
context?: JourneezeContext;
|
|
23
|
+
/** Reviewer token from the board's review link (`?jz_invite=`), when the reviewer was invited. */
|
|
24
|
+
reviewerToken?: string;
|
|
25
|
+
/** Told when the mode switches on or off, including from inside the widget (Esc, Exit, "Not now"). */
|
|
26
|
+
onModeChange?: (enabled: boolean) => void;
|
|
27
|
+
/**
|
|
28
|
+
* Told whether in-app help is available to the current user once `enableHelp()` has checked
|
|
29
|
+
* the site, and again if that changes (#142), so a host can hide its own Help control.
|
|
30
|
+
*/
|
|
31
|
+
onHelpStatus?: (status: HelpStatus) => void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Whether in-app help is available to the current user (#142). `disabled`: the site has help
|
|
35
|
+
* switched off. `audience`: the site's help is for reviewers only and this user isn't one.
|
|
36
|
+
*/
|
|
37
|
+
export type HelpStatus = {
|
|
38
|
+
available: true;
|
|
39
|
+
} | {
|
|
40
|
+
available: false;
|
|
41
|
+
reason: "disabled" | "audience";
|
|
42
|
+
};
|
|
43
|
+
export type Reaction = "like" | "dislike" | "understood" | "confused";
|
|
44
|
+
export interface JourneezeHandle {
|
|
45
|
+
/** Switch the feedback mode on: lazy-loads the widget, which asks for consent first. */
|
|
46
|
+
enable(): Promise<void>;
|
|
47
|
+
/** Switch the mode off and unmount the widget. Nothing is captured while off. */
|
|
48
|
+
disable(): void;
|
|
49
|
+
isEnabled(): boolean;
|
|
50
|
+
/** Show the help launcher: lazy-loads the help chunk. Reading guides needs no consent. */
|
|
51
|
+
enableHelp(): Promise<void>;
|
|
52
|
+
/** Remove the launcher and panel; nothing of the help chunk keeps running. */
|
|
53
|
+
disableHelp(): void;
|
|
54
|
+
isHelpEnabled(): boolean;
|
|
55
|
+
/** Update persona / journey step as the reviewer moves through the host app. */
|
|
56
|
+
setContext(context: JourneezeContext): void;
|
|
57
|
+
getContext(): Readonly<JourneezeContext>;
|
|
58
|
+
}
|
|
59
|
+
export { API_PATH_PATTERN, CONTEXT_FIELDS, CONTEXT_ID_PATTERN, CONTEXT_VALUE_MAX_LEN, SITE_KEY_PATTERN, USER_ID_PATTERN } from "./contract";
|
|
60
|
+
/** Where the collector is reached (§5 of the contract; transport in direct-mode-v1.md). */
|
|
61
|
+
export interface JourneezeTransportConfig {
|
|
62
|
+
/**
|
|
63
|
+
* The collector origin, e.g. `https://app.journeeze.dev` (direct mode, CORS for the site's
|
|
64
|
+
* allowlisted origins) or `https://www.example.com` (the site's same-origin proxy). An absolute
|
|
65
|
+
* `https:` origin; `http:` only for `localhost`/`127.0.0.1`. No path, query or fragment.
|
|
66
|
+
*/
|
|
67
|
+
apiHost?: string;
|
|
68
|
+
/**
|
|
69
|
+
* A path prefix under `apiHost` where the site mounts the collector paths, e.g. `/jz`
|
|
70
|
+
* (neutral words only: blockers drop paths containing `track`, `analytics` and similar).
|
|
71
|
+
* Starts with `/`, no trailing `/`, no query or fragment. Default: none.
|
|
72
|
+
*/
|
|
73
|
+
apiPath?: string;
|
|
74
|
+
}
|
|
75
|
+
/** Screenshot masking options (§6). They can only widen masking, never narrow it. */
|
|
76
|
+
export interface JourneezeMaskingConfig {
|
|
77
|
+
/**
|
|
78
|
+
* Extra CSS selectors masked in screenshots, on top of the fixed defaults (form fields,
|
|
79
|
+
* editable regions, `[data-jz-mask]`, media and frames). Masking misconfiguration fails closed:
|
|
80
|
+
* if any entry is not a valid selector, or matching it throws, the handle takes no screenshots
|
|
81
|
+
* at all (feedback is still sent without one) and the SDK warns once (contract §6, v1.1).
|
|
82
|
+
*/
|
|
83
|
+
maskSelectors?: readonly string[];
|
|
84
|
+
/** Replace every text run in screenshots with a box of the same size. Default `false`. */
|
|
85
|
+
maskAllText?: boolean;
|
|
86
|
+
}
|
|
87
|
+
/** What feedback sends as the page path (contract §2/§3, v1.1). */
|
|
88
|
+
export type JourneezePagePathMode = "redacted" | "none";
|
|
89
|
+
/**
|
|
90
|
+
* A raw event as the SDK's signal patterns see it (signals-v1.md). It never leaves the browser.
|
|
91
|
+
* `ts` is `performance.now()` time.
|
|
92
|
+
*/
|
|
93
|
+
export interface JourneezeSignalEvent {
|
|
94
|
+
kind: string;
|
|
95
|
+
ts: number;
|
|
96
|
+
fields: Readonly<Record<string, unknown>>;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The tflo module surface the SDK uses when the host injects its own (signals-v1.md §3.3,
|
|
100
|
+
* "capability floor"): the `tflo-browser-events@0.1.0` surface. Typed structurally so the SDK
|
|
101
|
+
* has no build-time dependency on the package.
|
|
102
|
+
*/
|
|
103
|
+
export interface JourneezeTfloModule {
|
|
104
|
+
TFloBrowser: unknown;
|
|
105
|
+
Pattern: unknown;
|
|
106
|
+
}
|
|
107
|
+
/** A site rule (signals-v1.md §4.2). The name must be registered for the site. */
|
|
108
|
+
export interface JourneezeSiteRule {
|
|
109
|
+
/** `^[a-z][a-z0-9_]{0,63}$`, registered for the site; never a platform name or `jz_*`. */
|
|
110
|
+
name: string;
|
|
111
|
+
/** A rule in the SDK's own CEL subset (signals-v1.md §4.2), or a predicate over raw events. */
|
|
112
|
+
when: string | ((event: JourneezeSignalEvent) => boolean);
|
|
113
|
+
/** Param mapping (`"event.<path>"`); keys must be in the registration's allowlist (≤ 8). */
|
|
114
|
+
params?: Readonly<Record<string, string>>;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Signals configuration. signals-v1.md (C0d) §3.2 is the normative definition. Signals run only
|
|
118
|
+
* with consent, help or feedback mode on and `enabled: true`; before that no tflo code is loaded.
|
|
119
|
+
*/
|
|
120
|
+
export interface JourneezeSignalsConfigV1 {
|
|
121
|
+
/** Signals are off unless `true`. Default `false`: the signals chunk never loads. */
|
|
122
|
+
enabled?: boolean;
|
|
123
|
+
/** Optional site rules. */
|
|
124
|
+
rules?: readonly JourneezeSiteRule[];
|
|
125
|
+
/** The host's own tflo module, or a lazy loader for it (`() => import("tflo-browser-events")`). */
|
|
126
|
+
tflo?: JourneezeTfloModule | (() => Promise<JourneezeTfloModule>);
|
|
127
|
+
}
|
|
128
|
+
/** SDK configuration, contract v1: today's options plus the v1 fields, all optional. */
|
|
129
|
+
export interface JourneezeConfigV1 extends Omit<JourneezeOptions, "endpoint">, JourneezeTransportConfig, JourneezeMaskingConfig {
|
|
130
|
+
/**
|
|
131
|
+
* Kept for v0 hosts: a full collector base URL (relative or absolute). When set it wins over
|
|
132
|
+
* `apiHost`/`apiPath`, and the SDK warns once.
|
|
133
|
+
* @deprecated Use `apiHost` (and `apiPath`).
|
|
134
|
+
*/
|
|
135
|
+
endpoint?: string;
|
|
136
|
+
/**
|
|
137
|
+
* The signed-in user's opaque id ({@link USER_ID_PATTERN}). Without it feedback is anonymous.
|
|
138
|
+
* An invalid value is dropped with a console warning and never sent.
|
|
139
|
+
*/
|
|
140
|
+
userId?: string;
|
|
141
|
+
/**
|
|
142
|
+
* Signed host identity (contract §4, v1.3): the host server's signature over `userId`, from
|
|
143
|
+
* the same authenticated `no-store` response. Unpadded base64url
|
|
144
|
+
* HMAC-SHA256(site identity secret, `"jz-id-v1\n" + siteKey + "\n" + userId + "\n" + userIdExp`).
|
|
145
|
+
* Sites that require signed identity ignore an unsigned or expired `userId` (the feedback is
|
|
146
|
+
* anonymous and carries no screenshot). The SDK never computes or refreshes it: re-supply it
|
|
147
|
+
* with {@link JourneezeHandleV1.setUser} before `userIdExp`.
|
|
148
|
+
*/
|
|
149
|
+
userIdSig?: string;
|
|
150
|
+
/** Expiry of {@link userIdSig}, Unix seconds (at most 24 h ahead). */
|
|
151
|
+
userIdExp?: number;
|
|
152
|
+
/**
|
|
153
|
+
* A CSP nonce set on every `<style>` the SDK injects (in its shadow roots), so hosts with
|
|
154
|
+
* `style-src 'nonce-…'` need no `'unsafe-inline'`.
|
|
155
|
+
*/
|
|
156
|
+
nonce?: string;
|
|
157
|
+
/**
|
|
158
|
+
* Honour Global Privacy Control (`navigator.globalPrivacyControl === true`): no help telemetry
|
|
159
|
+
* and no signals are sent. Feedback the user explicitly submits is not affected. Default `true`.
|
|
160
|
+
*/
|
|
161
|
+
respectGlobalPrivacyControl?: boolean;
|
|
162
|
+
/**
|
|
163
|
+
* `"redacted"` (default): send the page path with id-like segments replaced. `"none"`: send no
|
|
164
|
+
* page path; reviewers group by `journeyStep`. Hosts with route templates should use `"none"`.
|
|
165
|
+
*/
|
|
166
|
+
pagePath?: JourneezePagePathMode;
|
|
167
|
+
/** Signals (signals-v1.md §3.2): derived, aggregate-only signals; off unless `enabled`. */
|
|
168
|
+
signals?: JourneezeSignalsConfigV1;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The host's signature over a user id (contract §4, v1.3). Passing it again for the same id
|
|
172
|
+
* only refreshes the signature (no logout).
|
|
173
|
+
*/
|
|
174
|
+
export interface JourneezeUserIdSignature {
|
|
175
|
+
/** {@link JourneezeConfigV1.userIdSig}. */
|
|
176
|
+
sig: string;
|
|
177
|
+
/** {@link JourneezeConfigV1.userIdExp}, Unix seconds. */
|
|
178
|
+
exp: number;
|
|
179
|
+
}
|
|
180
|
+
/** The handle, contract v1: today's handle plus identity and logout. */
|
|
181
|
+
export interface JourneezeHandleV1 extends JourneezeHandle {
|
|
182
|
+
/**
|
|
183
|
+
* Set (or, with `undefined`, clear) the signed-in user's opaque id ({@link USER_ID_PATTERN}).
|
|
184
|
+
* Applies to feedback created from now on. Switching user is a logout, enforced here: a
|
|
185
|
+
* different id while one is set calls {@link reset} first; `undefined` while one is set is
|
|
186
|
+
* {@link reset}; the same id does nothing. An invalid id is treated as `undefined` and warns.
|
|
187
|
+
*/
|
|
188
|
+
setUser(userId: string | undefined, signature?: JourneezeUserIdSignature): void;
|
|
189
|
+
/**
|
|
190
|
+
* Call on logout (contract §8, v1.1). Sends this user's queued and stored feedback once (the
|
|
191
|
+
* reset-send rule), still attributed to them, then removes it from storage whatever the
|
|
192
|
+
* outcome; switches feedback mode off; clears the user id, the context, the reviewer token and
|
|
193
|
+
* the anonymous consent entry; drops queued help telemetry and signals and resets the signals'
|
|
194
|
+
* per-page-load counters and windows (v1 has no session id); and, if help was on, remounts it
|
|
195
|
+
* with empty state. Idempotent, safe on a handle that was never enabled, never
|
|
196
|
+
* throws. The React provider calls it itself when `userId` changes away from an id and on
|
|
197
|
+
* unmount.
|
|
198
|
+
*/
|
|
199
|
+
reset(): void;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Props of `<JourneezeProvider>` (contract §8, "Asynchronous configuration", v1.1-r2): the v1
|
|
203
|
+
* config with `siteKey` possibly `undefined` while the host's public config loads. While it is
|
|
204
|
+
* undefined the provider gives its children an inert handle (every method a no-op, nothing sent,
|
|
205
|
+
* nothing loaded) and `useJourneeze()` never throws; the first defined `siteKey` creates the real
|
|
206
|
+
* handle.
|
|
207
|
+
*/
|
|
208
|
+
export type JourneezeProviderPropsV1 = Omit<JourneezeConfigV1, "siteKey"> & {
|
|
209
|
+
siteKey: string | undefined;
|
|
210
|
+
/** The provider's children; typed structurally so this file needs no React types. */
|
|
211
|
+
children?: unknown;
|
|
212
|
+
};
|
|
213
|
+
/** Proto3 JSON enum names of `journeeze.collector.v1.Reaction`. */
|
|
214
|
+
export declare const REACTION_WIRE: Readonly<Record<Reaction, string>>;
|
|
215
|
+
/** Proto3 JSON enum names of `journeeze.collector.v1.FeedbackKind` (unspecified is omitted). */
|
|
216
|
+
export declare const FEEDBACK_KIND_WIRE: {
|
|
217
|
+
readonly request: "FEEDBACK_KIND_REQUEST";
|
|
218
|
+
readonly callout: "FEEDBACK_KIND_CALLOUT";
|
|
219
|
+
};
|
|
220
|
+
/**
|
|
221
|
+
* A help-originated feedback kind (north-star N4). Plain feedback has no kind: it omits the
|
|
222
|
+
* field (proto3 unspecified).
|
|
223
|
+
*/
|
|
224
|
+
export type FeedbackKind = "request" | "callout";
|
|
225
|
+
/** The wire name of a feedback kind. */
|
|
226
|
+
export type WireFeedbackKind = (typeof FEEDBACK_KIND_WIRE)[FeedbackKind];
|
|
227
|
+
export interface WireFeedbackContext {
|
|
228
|
+
persona?: string | undefined;
|
|
229
|
+
journeyId?: string | undefined;
|
|
230
|
+
journeyStep?: string | undefined;
|
|
231
|
+
tflowId?: string;
|
|
232
|
+
pagePath?: string;
|
|
233
|
+
device?: string | undefined;
|
|
234
|
+
businessType?: string | undefined;
|
|
235
|
+
state?: string | undefined;
|
|
236
|
+
}
|
|
237
|
+
export interface WireElementAnchor {
|
|
238
|
+
tflowId?: string;
|
|
239
|
+
cssSelector?: string;
|
|
240
|
+
xRatio: number;
|
|
241
|
+
yRatio: number;
|
|
242
|
+
}
|
|
243
|
+
export interface SubmitFeedbackRequest {
|
|
244
|
+
siteKey: string;
|
|
245
|
+
consentGiven: true;
|
|
246
|
+
context: WireFeedbackContext;
|
|
247
|
+
anchor?: WireElementAnchor;
|
|
248
|
+
reaction?: string;
|
|
249
|
+
comment?: string;
|
|
250
|
+
screenshotId?: string;
|
|
251
|
+
reviewerToken?: string;
|
|
252
|
+
/**
|
|
253
|
+
* Omitted = plain feedback. A request needs a comment; a callout needs an element anchor or a
|
|
254
|
+
* step of a guide shown on the site. Neither carries a reaction.
|
|
255
|
+
*/
|
|
256
|
+
kind?: WireFeedbackKind;
|
|
257
|
+
/**
|
|
258
|
+
* Direct-mode contract C0b v1.1 §7: a random UUIDv4 the outbox stamps once per submission and
|
|
259
|
+
* reuses on every retry; the collector answers a repeat with the original feedback id.
|
|
260
|
+
*/
|
|
261
|
+
submissionId?: string;
|
|
262
|
+
/**
|
|
263
|
+
* Contract C0a §4 (collector.proto `user_id = 10`): the host's opaque user id, fixed when the
|
|
264
|
+
* submission is written so a queued item replays under the id it was written with.
|
|
265
|
+
*/
|
|
266
|
+
userId?: string;
|
|
267
|
+
/** Signed host identity (C0a §4 v1.3, collector.proto `user_id_exp = 12`). */
|
|
268
|
+
userIdExp?: number;
|
|
269
|
+
/** Signed host identity (C0a §4 v1.3, collector.proto `user_id_sig = 13`). */
|
|
270
|
+
userIdSig?: string;
|
|
271
|
+
}
|
|
272
|
+
export interface SubmitFeedbackResponse {
|
|
273
|
+
feedbackId?: string;
|
|
274
|
+
}
|
|
275
|
+
export interface GetWidgetConfigResponse {
|
|
276
|
+
enabled?: boolean;
|
|
277
|
+
consentText?: string;
|
|
278
|
+
personas?: string[];
|
|
279
|
+
/** Feedback (help callouts and requests included) from a visitor with no reviewer token and no user id (ADR 0013 §4). */
|
|
280
|
+
anonymousAllowed?: boolean;
|
|
281
|
+
/** Who may read the site's help: "reviewers" or "public" (ADR 0013 §1). */
|
|
282
|
+
helpAudience?: string;
|
|
283
|
+
/** The site's published journeys and their checkpoints in order (signals-v1.md §4.1, §4.4; C0c § Step ids). */
|
|
284
|
+
publishedJourneys?: {
|
|
285
|
+
journeyId: string;
|
|
286
|
+
stepIds: string[];
|
|
287
|
+
}[];
|
|
288
|
+
/** Site signals registered for the site (signals-v1.md §4.2); site rules may emit only these. */
|
|
289
|
+
siteSignals?: {
|
|
290
|
+
name: string;
|
|
291
|
+
meaning?: string;
|
|
292
|
+
params?: {
|
|
293
|
+
name: string;
|
|
294
|
+
type: "int" | "double" | "bool" | "string";
|
|
295
|
+
}[];
|
|
296
|
+
}[];
|
|
297
|
+
/** The site's signals switch (signals-v1.md §4.4): when not true no signals chunk loads. */
|
|
298
|
+
signalsEnabled?: boolean;
|
|
299
|
+
}
|
|
300
|
+
/** `CreateScreenshotUpload` (collector.proto): reserve a masked PNG/WebP upload, at most 2 MiB. */
|
|
301
|
+
export interface CreateScreenshotUploadRequest {
|
|
302
|
+
siteKey: string;
|
|
303
|
+
contentType: "image/webp" | "image/png";
|
|
304
|
+
/** Exact size of the bytes that will be PUT. */
|
|
305
|
+
byteLength: number;
|
|
306
|
+
reviewerToken?: string;
|
|
307
|
+
/** Opaque host user id (C0a §4) for an attributed end user's upload (#141). */
|
|
308
|
+
userId?: string;
|
|
309
|
+
/** Required with `userId` and no `reviewerToken`. */
|
|
310
|
+
consentGiven?: boolean;
|
|
311
|
+
/** Signed host identity (C0a §4 v1.3, collector.proto `user_id_exp = 7`). */
|
|
312
|
+
userIdExp?: number;
|
|
313
|
+
/** Signed host identity (C0a §4 v1.3, collector.proto `user_id_sig = 8`). */
|
|
314
|
+
userIdSig?: string;
|
|
315
|
+
}
|
|
316
|
+
/** `GetHelpIndex` (collector.proto): the site's published guides. Read-only, no consent needed. */
|
|
317
|
+
export interface GetHelpIndexRequest {
|
|
318
|
+
siteKey: string;
|
|
319
|
+
}
|
|
320
|
+
export interface HelpIndexEntry {
|
|
321
|
+
journeyId?: string;
|
|
322
|
+
/** The journey's stable id within its project (e.g. `r1-j09`). */
|
|
323
|
+
slug?: string;
|
|
324
|
+
title?: string;
|
|
325
|
+
goal?: string;
|
|
326
|
+
stepTitles?: string[];
|
|
327
|
+
/** Changes whenever the guide changes. */
|
|
328
|
+
version?: string;
|
|
329
|
+
}
|
|
330
|
+
export interface GetHelpIndexResponse {
|
|
331
|
+
guides?: HelpIndexEntry[];
|
|
332
|
+
/** Changes whenever any guide changes (also the ETag). */
|
|
333
|
+
version?: string;
|
|
334
|
+
}
|
|
335
|
+
/** `GetGuide` (collector.proto): one published guide with short-lived same-origin asset links. */
|
|
336
|
+
export interface GetGuideRequest {
|
|
337
|
+
siteKey: string;
|
|
338
|
+
journeyId: string;
|
|
339
|
+
}
|
|
340
|
+
export interface GuideStep {
|
|
341
|
+
index?: number;
|
|
342
|
+
title?: string;
|
|
343
|
+
instruction?: string;
|
|
344
|
+
/** The element the step acts on (`data-tflow-id`), if the app names it. */
|
|
345
|
+
tflowId?: string;
|
|
346
|
+
/** Path relative to the collector endpoint; expires. */
|
|
347
|
+
screenshotUrl?: string;
|
|
348
|
+
videoStartMs?: number;
|
|
349
|
+
videoEndMs?: number;
|
|
350
|
+
/** Every way to find the element, most stable first (contract C0c v1.1 § Targets). */
|
|
351
|
+
targets?: GuideTarget[];
|
|
352
|
+
/** The host's `journeyStep` id for this step (C0c v1.1-r2), when known. */
|
|
353
|
+
stepRef?: string;
|
|
354
|
+
}
|
|
355
|
+
/** One way to find a guide step's element. */
|
|
356
|
+
export interface GuideTarget {
|
|
357
|
+
/** tflow, testId, role or css. */
|
|
358
|
+
strategy?: string;
|
|
359
|
+
/** The tflo id, test id, ARIA role or CSS selector. */
|
|
360
|
+
value?: string;
|
|
361
|
+
/** For role: the accessible name. */
|
|
362
|
+
name?: string;
|
|
363
|
+
}
|
|
364
|
+
export interface GetGuideResponse {
|
|
365
|
+
journeyId?: string;
|
|
366
|
+
slug?: string;
|
|
367
|
+
title?: string;
|
|
368
|
+
goal?: string;
|
|
369
|
+
steps?: GuideStep[];
|
|
370
|
+
/** Paths relative to the collector endpoint; they expire (fetch the guide again). */
|
|
371
|
+
videoUrl?: string;
|
|
372
|
+
captionsUrl?: string;
|
|
373
|
+
/** The guide revision (C0c v1.1-r2): 1 when created, +1 when its content is replaced. */
|
|
374
|
+
rev?: number;
|
|
375
|
+
/** Changes when the guide or its asset links change (also the ETag). */
|
|
376
|
+
version?: string;
|
|
377
|
+
/** The guide content the steps show (16 hex); help events quote it (ADR 0011). */
|
|
378
|
+
contentVersion?: string;
|
|
379
|
+
/** RFC 3339 time the asset links stop working; fetch the guide again before then. */
|
|
380
|
+
linksExpireAt?: string;
|
|
381
|
+
}
|
|
382
|
+
export interface CreateScreenshotUploadResponse {
|
|
383
|
+
/** Pass as `SubmitFeedbackRequest.screenshotId` once the upload succeeded. */
|
|
384
|
+
screenshotId: string;
|
|
385
|
+
/** Path relative to the collector endpoint to PUT the bytes to. */
|
|
386
|
+
uploadUrl: string;
|
|
387
|
+
/** Headers to send with the PUT (content type and a single-use upload token). */
|
|
388
|
+
headers?: Record<string, string>;
|
|
389
|
+
/** RFC 3339 time after which the upload is refused. */
|
|
390
|
+
expiresAt?: string;
|
|
391
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { WireElementAnchor } from "../types";
|
|
2
|
+
/** `main > section:nth-of-type(2) > button`, relative to the nearest tflo element (or `body`). */
|
|
3
|
+
export declare function structuralSelector(el: Element): string | undefined;
|
|
4
|
+
/** The anchor for a click at (`x`, `y`) viewport coordinates on `el`. */
|
|
5
|
+
export declare function anchorFor(el: Element, x: number, y: number): WireElementAnchor;
|
|
6
|
+
/** Short label for the pointing tag: the tflo id, else the tag name. */
|
|
7
|
+
export declare function elementLabel(el: Element): string;
|
|
8
|
+
/** `[data-jz-block]` regions (contract §10) are left out of screenshots and cannot be anchored. */
|
|
9
|
+
export declare function isBlocked(el: Element): boolean;
|
|
10
|
+
/** Device class from the viewport width, when the host did not declare one. */
|
|
11
|
+
export declare function deviceClass(width: number): "mobile" | "tablet" | "desktop";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Screenshot masking options (contract §6): they can only widen masking, never narrow it. */
|
|
2
|
+
export interface MaskOptions {
|
|
3
|
+
/** Extra selectors replaced by a placeholder, exactly like form fields. Invalid entries warn once. */
|
|
4
|
+
maskSelectors?: readonly string[];
|
|
5
|
+
/** Replace every text run with a same-size box. Default `false`. */
|
|
6
|
+
maskAllText?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare class MaskingError extends Error {
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Clone hooks for modern-screenshot. `filter` runs on every child before it is cloned (pre-order);
|
|
12
|
+
* `onCloneEachNode` runs when a node's clone is complete (post-order), so a stack pairs each clone
|
|
13
|
+
* with its source. Text nodes are filtered but get no clone callback, so they are not stacked.
|
|
14
|
+
*/
|
|
15
|
+
export declare function maskingHooks(root: Node, opts?: MaskOptions): {
|
|
16
|
+
filter: (node: Node) => boolean;
|
|
17
|
+
onCloneEachNode: (clone: Node) => void;
|
|
18
|
+
onCloneNode: (clone: Node) => void;
|
|
19
|
+
failure: () => string | undefined;
|
|
20
|
+
};
|
|
21
|
+
/** A hatched box with the masked element's geometry. Reads layout only, never content. */
|
|
22
|
+
export declare function placeholderFor(el: Element, doc: Document): HTMLElement;
|
|
23
|
+
/** A transparent box with a blocked element's geometry: it holds the space, with no content. */
|
|
24
|
+
export declare function emptyBoxFor(el: Element, doc: Document): HTMLElement;
|
|
25
|
+
/** The visible viewport as a masked WebP (PNG where WebP encoding is unsupported), ≤ 2 MB. */
|
|
26
|
+
export declare function captureViewport(root?: Element, opts?: MaskOptions): Promise<Blob | undefined>;
|
|
27
|
+
/** The masked clone as SVG markup (what gets rasterised). Used by the e2e privacy checks. */
|
|
28
|
+
export declare function maskedSnapshotSvg(root?: Element): Promise<string>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare const CONSENT_VERSION = 1;
|
|
2
|
+
/**
|
|
3
|
+
* Default consent copy (the site's `GetWidgetConfig.consentText` replaces it). One consent per
|
|
4
|
+
* site covers both the feedback mode and in-app help check-ins, so both show this same text.
|
|
5
|
+
*/
|
|
6
|
+
export declare const DEFAULT_CONSENT = "Feedback mode captures the element you point at, a screenshot of this page with form fields hidden, your comment, and your persona and journey step. It never records keystrokes, form contents or query strings. Your feedback is visible to the project's review team.";
|
|
7
|
+
/** Default copy for a fully anonymous visitor, who never sends a screenshot (ADR 0013 §4). */
|
|
8
|
+
export declare const DEFAULT_CONSENT_NO_SCREENSHOT = "Feedback mode captures the element you point at, your comment, and your persona and journey step. No screenshot is sent. It never records keystrokes, form contents or query strings. Your feedback is visible to the project's review team.";
|
|
9
|
+
/**
|
|
10
|
+
* Whose consent (contract §7, §9): per user when the host passes a `userId` (so on a shared device
|
|
11
|
+
* the next user is asked again), otherwise the site's anonymous entry.
|
|
12
|
+
*/
|
|
13
|
+
export declare const consentScope: (o: {
|
|
14
|
+
siteKey: string;
|
|
15
|
+
userId?: string | undefined;
|
|
16
|
+
}) => string;
|
|
17
|
+
/** Forgets the site's anonymous consent (`reset()`, a logout). Users' own entries stay. */
|
|
18
|
+
export declare function clearConsent(siteKey: string, storage?: Storage | undefined): void;
|
|
19
|
+
export declare function safeLocalStorage(): Storage | undefined;
|
|
20
|
+
/** `storage` null: memory only (an anonymous reader of public help, ADR 0013 §3). */
|
|
21
|
+
export declare function hasConsent(scope: string, storage?: Storage | null | undefined): boolean;
|
|
22
|
+
/** `storage` null: remembered for this page only (ADR 0013 §3). */
|
|
23
|
+
export declare function storeConsent(scope: string, storage?: Storage | null | undefined): void;
|
|
24
|
+
/** Test helper: forget in-memory consent. */
|
|
25
|
+
export declare function resetConsentMemory(): void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { MountOptions } from "../loader";
|
|
2
|
+
import type { GetWidgetConfigResponse, JourneezeOptions } from "../types";
|
|
3
|
+
import { CollectorError } from "../wire";
|
|
4
|
+
import { Outbox } from "./outbox";
|
|
5
|
+
export interface FeedbackChannel {
|
|
6
|
+
readonly outbox: Outbox;
|
|
7
|
+
/** Removes the page listeners and stores what is unsent for a later widget or help. */
|
|
8
|
+
dispose(): void;
|
|
9
|
+
}
|
|
10
|
+
export interface FeedbackChannelHooks {
|
|
11
|
+
/** Runs on pagehide before the outbox is persisted (the widget stashes pending uploads). */
|
|
12
|
+
beforePersist?: () => void;
|
|
13
|
+
onDropped?: (error: CollectorError) => void;
|
|
14
|
+
/**
|
|
15
|
+
* Keep unsent items in memory only, never in browser storage: an anonymous reader of public
|
|
16
|
+
* help (ADR 0013 §3). Unsent items are then lost when the page goes away.
|
|
17
|
+
*/
|
|
18
|
+
memoryOnly?: boolean;
|
|
19
|
+
}
|
|
20
|
+
type ChannelOptions = JourneezeOptions & Partial<Pick<MountOptions, "userId" | "pagePath" | "apiHost" | "apiPath">>;
|
|
21
|
+
/**
|
|
22
|
+
* Whose outbox items these are (contract §9, v1.1): the reviewer token on reviewer sites, else the
|
|
23
|
+
* signed-in user, else anonymous. A page claims only its own owner's items.
|
|
24
|
+
*/
|
|
25
|
+
export declare const ownerOf: (o: ChannelOptions) => string;
|
|
26
|
+
/**
|
|
27
|
+
* The storage half of `reset()` (contract §8, v1.1), loaded only at logout: this owner's stored
|
|
28
|
+
* feedback is removed from the device now and sent once (keepalive within the page-end budget,
|
|
29
|
+
* else plain fetch); a send that fails is lost, by design, so no comment text of the user stays at
|
|
30
|
+
* rest on a shared device. The anonymous consent is forgotten (the loader clears the reviewer token).
|
|
31
|
+
*/
|
|
32
|
+
export declare function logout(options: ChannelOptions): Promise<void>;
|
|
33
|
+
export declare function feedbackChannel(options: ChannelOptions, endpoint: string, hooks?: FeedbackChannelHooks): FeedbackChannel;
|
|
34
|
+
/** The site's public widget configuration (consent copy, personas). */
|
|
35
|
+
export declare function widgetConfig(endpoint: string, siteKey: string): Promise<GetWidgetConfigResponse>;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
declare const PATHS: {
|
|
2
|
+
readonly like: "<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M8 14s1.5 2 4 2 4-2 4-2\"/><path d=\"M9 9h.01\"/><path d=\"M15 9h.01\"/>";
|
|
3
|
+
readonly dislike: "<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M16 16s-1.5-2-4-2-4 2-4 2\"/><path d=\"M9 9h.01\"/><path d=\"M15 9h.01\"/>";
|
|
4
|
+
readonly understood: "<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m9 12 2 2 4-4\"/>";
|
|
5
|
+
readonly confused: "<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3\"/><path d=\"M12 17h.01\"/>";
|
|
6
|
+
readonly close: "<path d=\"M18 6 6 18\"/><path d=\"m6 6 12 12\"/>";
|
|
7
|
+
readonly persona: "<path d=\"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\"/><circle cx=\"12\" cy=\"7\" r=\"4\"/>";
|
|
8
|
+
readonly step: "<path d=\"M5 12h14\"/><path d=\"m12 5 7 7-7 7\"/>";
|
|
9
|
+
readonly info: "<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4\"/><path d=\"M12 8h.01\"/>";
|
|
10
|
+
};
|
|
11
|
+
export type IconName = keyof typeof PATHS;
|
|
12
|
+
/** A decorative (aria-hidden) inline SVG icon. Static markup only. */
|
|
13
|
+
export declare function icon(name: IconName, size?: number): SVGSVGElement;
|
|
14
|
+
export {};
|