@loupfeed/core 0.1.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/LICENSE.md +114 -0
- package/dist/index.cjs +942 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +351 -0
- package/dist/index.d.ts +351 -0
- package/dist/index.js +910 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSN parsing + ingest endpoint derivation.
|
|
3
|
+
*
|
|
4
|
+
* Format (Sentry-inspired):
|
|
5
|
+
* https://<PUBLIC_KEY>@<INGEST_HOST>/o/<ORG_ID>/p/<PROJECT_ID>
|
|
6
|
+
*
|
|
7
|
+
* Self-host vs SaaS is purely the host — same protocol, same packages.
|
|
8
|
+
*/
|
|
9
|
+
type Dsn = {
|
|
10
|
+
raw: string;
|
|
11
|
+
protocol: string;
|
|
12
|
+
publicKey: string;
|
|
13
|
+
/** hostname only (no port). */
|
|
14
|
+
host: string;
|
|
15
|
+
port: string;
|
|
16
|
+
orgId: string;
|
|
17
|
+
projectId: string;
|
|
18
|
+
/** `${protocol}://${host[:port]}` — no trailing slash. */
|
|
19
|
+
baseUrl: string;
|
|
20
|
+
};
|
|
21
|
+
declare function parseDsn(dsn: string): Dsn;
|
|
22
|
+
declare function eventsUrl(dsn: Dsn): string;
|
|
23
|
+
declare function replaysUrl(dsn: Dsn): string;
|
|
24
|
+
declare function manifestsUrl(dsn: Dsn): string;
|
|
25
|
+
declare function resolveUrl(dsn: Dsn): string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Public type surface for @loupfeed/core. The on-the-wire `FeedbackEvent` is
|
|
29
|
+
* specified in docs/04-context-api.md (§The FeedbackEvent envelope).
|
|
30
|
+
*/
|
|
31
|
+
/** Allowed tag values. `null`/`undefined` are dropped at serialization. */
|
|
32
|
+
type Primitive = string | number | boolean | null | undefined;
|
|
33
|
+
type User = {
|
|
34
|
+
id?: string;
|
|
35
|
+
email?: string;
|
|
36
|
+
username?: string;
|
|
37
|
+
ip_address?: string;
|
|
38
|
+
/** Arbitrary developer-supplied fields (plan, role, …). */
|
|
39
|
+
[key: string]: unknown;
|
|
40
|
+
};
|
|
41
|
+
type BreadcrumbLevel = 'debug' | 'info' | 'warning' | 'error';
|
|
42
|
+
type Breadcrumb = {
|
|
43
|
+
/** ISO timestamp; filled in automatically if omitted. */
|
|
44
|
+
timestamp?: string;
|
|
45
|
+
/** 'navigation' | 'ui.click' | 'http' | 'console' | custom. */
|
|
46
|
+
category: string;
|
|
47
|
+
type?: string;
|
|
48
|
+
message?: string;
|
|
49
|
+
level?: BreadcrumbLevel;
|
|
50
|
+
data?: Record<string, unknown>;
|
|
51
|
+
};
|
|
52
|
+
type ElementRect = {
|
|
53
|
+
x: number;
|
|
54
|
+
y: number;
|
|
55
|
+
w: number;
|
|
56
|
+
h: number;
|
|
57
|
+
};
|
|
58
|
+
/** Element anchor captured by the selector engine. */
|
|
59
|
+
type ElementInfo = {
|
|
60
|
+
/** data-fb-id | structural CSS path, relative to the selector root. */
|
|
61
|
+
selector: string;
|
|
62
|
+
/** Opaque build-injected `data-fb` id → resolves to src:line server-side. */
|
|
63
|
+
elementId?: string;
|
|
64
|
+
tagName?: string;
|
|
65
|
+
/** Truncated, masking-aware. */
|
|
66
|
+
text?: string;
|
|
67
|
+
rect?: ElementRect;
|
|
68
|
+
};
|
|
69
|
+
type FeedbackInput = {
|
|
70
|
+
text: string;
|
|
71
|
+
/** The clicked DOM element (the web overlay supplies it). */
|
|
72
|
+
element?: Element;
|
|
73
|
+
/** Non-DOM platforms (React Native) supply the opaque id directly. */
|
|
74
|
+
elementId?: string;
|
|
75
|
+
/** Non-DOM platforms supply a prebuilt anchor (tag/rect/selector) directly. */
|
|
76
|
+
elementInfo?: Partial<ElementInfo>;
|
|
77
|
+
/** Screenshots etc. (uploaded separately by the transport). */
|
|
78
|
+
attachments?: Blob[];
|
|
79
|
+
};
|
|
80
|
+
type AttachmentRef = {
|
|
81
|
+
type: 'image';
|
|
82
|
+
blobId: string;
|
|
83
|
+
};
|
|
84
|
+
/** The on-the-wire envelope ingested by the backend. */
|
|
85
|
+
type FeedbackEvent = {
|
|
86
|
+
eventId: string;
|
|
87
|
+
timestamp: string;
|
|
88
|
+
/** version@commit — the manifest join key. */
|
|
89
|
+
release: string;
|
|
90
|
+
environment: string;
|
|
91
|
+
text: string;
|
|
92
|
+
attachments?: AttachmentRef[];
|
|
93
|
+
element: ElementInfo;
|
|
94
|
+
user?: User;
|
|
95
|
+
contexts?: Record<string, Record<string, unknown>>;
|
|
96
|
+
tags?: Record<string, string | number | boolean>;
|
|
97
|
+
breadcrumbs?: Breadcrumb[];
|
|
98
|
+
request?: {
|
|
99
|
+
url: string;
|
|
100
|
+
route?: string;
|
|
101
|
+
};
|
|
102
|
+
replay?: {
|
|
103
|
+
replayId: string;
|
|
104
|
+
durationMs: number;
|
|
105
|
+
};
|
|
106
|
+
sdk?: {
|
|
107
|
+
name: string;
|
|
108
|
+
version: string;
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
/** Session-replay masking config (replay itself is deferred out of MVP). */
|
|
112
|
+
type ReplayOptions = {
|
|
113
|
+
maskAllText?: boolean;
|
|
114
|
+
maskAllInputs?: boolean;
|
|
115
|
+
blockSelector?: string;
|
|
116
|
+
maskTextSelector?: string;
|
|
117
|
+
};
|
|
118
|
+
type ReplayBuffer = {
|
|
119
|
+
replayId: string;
|
|
120
|
+
durationMs: number;
|
|
121
|
+
/**
|
|
122
|
+
* Replay codec:
|
|
123
|
+
* - 'rrweb' — web DOM (default)
|
|
124
|
+
* - 'wireframe' — React Native view-hierarchy snapshots (maskable, pure-JS)
|
|
125
|
+
* - 'video' — native screen recording (optional, pixels)
|
|
126
|
+
*/
|
|
127
|
+
kind?: 'rrweb' | 'wireframe' | 'video';
|
|
128
|
+
/** rrweb events (kind: 'rrweb'). */
|
|
129
|
+
events?: unknown[];
|
|
130
|
+
/** view-hierarchy frames (kind: 'wireframe'). */
|
|
131
|
+
frames?: unknown[];
|
|
132
|
+
/** recorded viewport, for wireframe reconstruction. */
|
|
133
|
+
viewport?: {
|
|
134
|
+
width: number;
|
|
135
|
+
height: number;
|
|
136
|
+
};
|
|
137
|
+
/** base64-encoded video (kind: 'video'). */
|
|
138
|
+
video?: string;
|
|
139
|
+
mimeType?: string;
|
|
140
|
+
};
|
|
141
|
+
/** Pluggable session recorder. Web = rrweb; React Native = native screen capture. */
|
|
142
|
+
interface Recorder {
|
|
143
|
+
start(): void;
|
|
144
|
+
stop(): void;
|
|
145
|
+
isRecording(): boolean;
|
|
146
|
+
/** Freeze the rolling buffer for the current event (may be async for native capture). */
|
|
147
|
+
snapshot(): ReplayBuffer | Promise<ReplayBuffer>;
|
|
148
|
+
}
|
|
149
|
+
/** Pluggable transport — override for self-host / proxy. */
|
|
150
|
+
interface Transport {
|
|
151
|
+
/** Send one event; resolves with the server-assigned eventId. */
|
|
152
|
+
send(event: FeedbackEvent): Promise<string>;
|
|
153
|
+
/** Upload a frozen replay buffer; resolves with its id + duration. */
|
|
154
|
+
sendReplay?(buffer: ReplayBuffer): Promise<{
|
|
155
|
+
replayId: string;
|
|
156
|
+
durationMs: number;
|
|
157
|
+
}>;
|
|
158
|
+
/** Wait for in-flight sends to settle (up to `timeout` ms). */
|
|
159
|
+
flush(timeout?: number): Promise<boolean>;
|
|
160
|
+
close?(): void;
|
|
161
|
+
}
|
|
162
|
+
type InitOptions = {
|
|
163
|
+
dsn: string;
|
|
164
|
+
environment?: string;
|
|
165
|
+
/** version@commit; build plugin can inject this. Defaults to ''. */
|
|
166
|
+
release?: string;
|
|
167
|
+
/** Fraction of feedbacks that attach a replay. 0..1. (Replay deferred.) */
|
|
168
|
+
replaysSampleRate?: number;
|
|
169
|
+
/** Rolling replay buffer length in seconds. Default 30. (Replay deferred.) */
|
|
170
|
+
replayBufferSeconds?: number;
|
|
171
|
+
replay?: ReplayOptions;
|
|
172
|
+
/** Auto-attach route, viewport, release. Default true. */
|
|
173
|
+
autoContext?: boolean;
|
|
174
|
+
/** Max breadcrumbs retained on the global scope. Default 50. */
|
|
175
|
+
maxBreadcrumbs?: number;
|
|
176
|
+
/** Last-chance scrub. Return null to drop the event. */
|
|
177
|
+
beforeSend?: (event: FeedbackEvent) => FeedbackEvent | null;
|
|
178
|
+
/** Override the transport (self-host / proxy / testing). */
|
|
179
|
+
transport?: Transport;
|
|
180
|
+
debug?: boolean;
|
|
181
|
+
};
|
|
182
|
+
type OverlayOptions = {
|
|
183
|
+
/** Root the selector engine generates paths relative to. Defaults to body. */
|
|
184
|
+
root?: Element | null;
|
|
185
|
+
/** Called when a user submits feedback. Defaults to core's `captureFeedback`. */
|
|
186
|
+
onSubmit?: (input: FeedbackInput) => void | Promise<void>;
|
|
187
|
+
/** Accent color (6-digit hex). Default '#6d28d9'. */
|
|
188
|
+
accentColor?: string;
|
|
189
|
+
/** z-index of the overlay host. Default 2147483600. */
|
|
190
|
+
zIndex?: number;
|
|
191
|
+
/** Show the built-in floating launcher button. Default true. */
|
|
192
|
+
launcher?: boolean;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Client — holds the parsed DSN, normalized options, and the transport. Owns
|
|
197
|
+
* `captureFeedback`: it builds the event from the active scope and sends it.
|
|
198
|
+
*/
|
|
199
|
+
|
|
200
|
+
declare class Client {
|
|
201
|
+
readonly dsn: Dsn;
|
|
202
|
+
private readonly options;
|
|
203
|
+
private readonly transport;
|
|
204
|
+
constructor(options: InitOptions);
|
|
205
|
+
getOptions(): InitOptions;
|
|
206
|
+
getDsn(): Dsn;
|
|
207
|
+
getTransport(): Transport;
|
|
208
|
+
captureFeedback(input: FeedbackInput): Promise<string>;
|
|
209
|
+
private maybeAttachReplay;
|
|
210
|
+
flush(timeout?: number): Promise<boolean>;
|
|
211
|
+
close(timeout?: number): Promise<boolean>;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The init/singleton wiring. `init()` is idempotent and safe to import on the
|
|
216
|
+
* server — no DOM work happens here (the overlay mounts separately, lazily).
|
|
217
|
+
*/
|
|
218
|
+
|
|
219
|
+
declare function init(options: InitOptions): Client;
|
|
220
|
+
declare function getClient(): Client | undefined;
|
|
221
|
+
declare function captureFeedback(input: FeedbackInput): Promise<string>;
|
|
222
|
+
/** Flush + stop. */
|
|
223
|
+
declare function close(timeout?: number): Promise<boolean>;
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Scope / context API — generalizes what Trivision's FeedbackProvider hard-codes
|
|
227
|
+
* (tenant/rep/version) into a Sentry-style developer-driven API.
|
|
228
|
+
*
|
|
229
|
+
* - The global scope holds long-lived data (user, tags, persistent contexts).
|
|
230
|
+
* - `withScope(cb)` forks a child for a single capture without polluting global.
|
|
231
|
+
*/
|
|
232
|
+
|
|
233
|
+
interface ScopeData {
|
|
234
|
+
user?: User;
|
|
235
|
+
tags: Record<string, Primitive>;
|
|
236
|
+
contexts: Record<string, Record<string, unknown>>;
|
|
237
|
+
breadcrumbs: Breadcrumb[];
|
|
238
|
+
}
|
|
239
|
+
declare class Scope {
|
|
240
|
+
private _user?;
|
|
241
|
+
private _tags;
|
|
242
|
+
private _contexts;
|
|
243
|
+
private _breadcrumbs;
|
|
244
|
+
private _maxBreadcrumbs;
|
|
245
|
+
setUser(user: User | null): this;
|
|
246
|
+
getUser(): User | undefined;
|
|
247
|
+
setTag(key: string, value: Primitive): this;
|
|
248
|
+
setTags(tags: Record<string, Primitive>): this;
|
|
249
|
+
setContext(key: string, data: Record<string, unknown> | null): this;
|
|
250
|
+
addBreadcrumb(breadcrumb: Breadcrumb, maxBreadcrumbs?: number): this;
|
|
251
|
+
setMaxBreadcrumbs(n: number): this;
|
|
252
|
+
clearBreadcrumbs(): this;
|
|
253
|
+
clone(): Scope;
|
|
254
|
+
getScopeData(): ScopeData;
|
|
255
|
+
}
|
|
256
|
+
declare function getGlobalScope(): Scope;
|
|
257
|
+
/** The active scope: the top of the withScope stack, else the global scope. */
|
|
258
|
+
declare function getCurrentScope(): Scope;
|
|
259
|
+
/** Fork a child scope for a single capture; auto-popped when `cb` returns. */
|
|
260
|
+
declare function withScope<T>(cb: (scope: Scope) => T): T;
|
|
261
|
+
declare function setUser(user: User | null): void;
|
|
262
|
+
declare function setContext(key: string, data: Record<string, unknown> | null): void;
|
|
263
|
+
declare function setTag(key: string, value: Primitive): void;
|
|
264
|
+
declare function setTags(tags: Record<string, Primitive>): void;
|
|
265
|
+
declare function addBreadcrumb(breadcrumb: Breadcrumb): void;
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Session recorder — wraps rrweb behind loupfeed's own rolling-buffer + masking
|
|
269
|
+
* lifecycle (docs/05). We own the masking *defaults* (mask-by-default), the ring
|
|
270
|
+
* buffer, and the upload lifecycle; rrweb is just the DOM codec, swappable later.
|
|
271
|
+
*
|
|
272
|
+
* rrweb is loaded with a real dynamic import so it (a) stays out of apps that
|
|
273
|
+
* don't enable replay (tree-shakeable / lazy) and (b) is bundler-resolved on web.
|
|
274
|
+
* React Native uses a native recorder instead and stubs `rrweb` in Metro.
|
|
275
|
+
*/
|
|
276
|
+
|
|
277
|
+
/** The public `replay` API (re-exported from core's index). */
|
|
278
|
+
declare const replay: {
|
|
279
|
+
start(): void;
|
|
280
|
+
stop(): void;
|
|
281
|
+
isRecording(): boolean;
|
|
282
|
+
/** Freeze the rolling buffer for the current event. */
|
|
283
|
+
snapshot(): ReplayBuffer;
|
|
284
|
+
};
|
|
285
|
+
declare function setReplayRecorder(recorder: Recorder | null): void;
|
|
286
|
+
declare function getActiveRecorder(): Recorder;
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Element-identification engine (lifted + generalized from the Trivision
|
|
290
|
+
* `app/src/feedback/selector.ts`). Produces an identifier per element that:
|
|
291
|
+
* 1. Survives re-renders within a session
|
|
292
|
+
* 2. Survives a page refresh (comments persist across reloads)
|
|
293
|
+
* 3. Tolerates minor markup changes between deploys (best-effort)
|
|
294
|
+
*
|
|
295
|
+
* Three-tier resolution, in priority order:
|
|
296
|
+
* 1. el.closest('[data-fb-id]') → hand-authored stable id
|
|
297
|
+
* 2. el.closest('[data-fb]') → opaque build-injected id → manifest (src:line)
|
|
298
|
+
* 3. structural CSS path → tag + :nth-of-type(n), relative to the root
|
|
299
|
+
*
|
|
300
|
+
* The opaque `data-fb` id never carries a source path; it resolves to
|
|
301
|
+
* `<rel-path>:<line>` server-side via the manifest. The client never sees paths.
|
|
302
|
+
*/
|
|
303
|
+
|
|
304
|
+
declare function setSelectorRoot(el: Element | null): void;
|
|
305
|
+
declare function getSelectorRoot(): Element | null;
|
|
306
|
+
/** Generate a stable selector for an element, relative to the root. */
|
|
307
|
+
declare function selectorFor(el: Element): string;
|
|
308
|
+
/** Try to resolve a stored selector back to a live element. */
|
|
309
|
+
declare function resolveSelector(selector: string): Element | null;
|
|
310
|
+
/** Snapshot an element's identity + display metadata at click time. */
|
|
311
|
+
declare function snapshotElement(el: Element): ElementInfo;
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Framework-agnostic inspector overlay, rendered into a shadow root so the host
|
|
315
|
+
* app's CSS can neither style nor break it. Generalizes the hit-testing core of
|
|
316
|
+
* Trivision's FeedbackOverlay (elementsFromPoint + pointer-events isolation +
|
|
317
|
+
* portal) into vanilla DOM with zero runtime deps.
|
|
318
|
+
*
|
|
319
|
+
* Capture-only by design: the in-page widget collects element-anchored feedback;
|
|
320
|
+
* triage/threads/replay live in the dashboard (doc 06), not the host page.
|
|
321
|
+
*
|
|
322
|
+
* State machine: idle → inspect (hover-highlight) → compose (comment popup).
|
|
323
|
+
*/
|
|
324
|
+
|
|
325
|
+
type OverlayController = {
|
|
326
|
+
open(): void;
|
|
327
|
+
close(): void;
|
|
328
|
+
toggleInspect(): void;
|
|
329
|
+
isInspecting(): boolean;
|
|
330
|
+
destroy(): void;
|
|
331
|
+
};
|
|
332
|
+
/** Mount the overlay into the DOM. Returns an unmount function. SSR-safe (no-op). */
|
|
333
|
+
declare function mountOverlay(opts?: OverlayOptions): () => void;
|
|
334
|
+
/** Singleton control surface targeting the most recently mounted overlay. */
|
|
335
|
+
declare const overlay: Omit<OverlayController, 'destroy'>;
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Fetch transport — POSTs the FeedbackEvent envelope (and, when present, the
|
|
339
|
+
* replay blob) to the DSN-derived ingest endpoints. Retries on network / 5xx,
|
|
340
|
+
* tracks in-flight sends for flush(), and uses `keepalive` so a submit can
|
|
341
|
+
* outlive an unload.
|
|
342
|
+
*/
|
|
343
|
+
|
|
344
|
+
declare function makeFetchTransport(dsn: Dsn, opts?: {
|
|
345
|
+
debug?: boolean;
|
|
346
|
+
}): Transport;
|
|
347
|
+
|
|
348
|
+
/** SDK version, surfaced on every event's `sdk` field. Bumped by Changesets. */
|
|
349
|
+
declare const SDK_VERSION = "0.0.0";
|
|
350
|
+
|
|
351
|
+
export { type AttachmentRef, type Breadcrumb, type BreadcrumbLevel, Client, type Dsn, type ElementInfo, type ElementRect, type FeedbackEvent, type FeedbackInput, type InitOptions, type OverlayController, type OverlayOptions, type Primitive, type Recorder, type ReplayBuffer, type ReplayOptions, SDK_VERSION, Scope, type ScopeData, type Transport, type User, addBreadcrumb, captureFeedback, close, eventsUrl, getActiveRecorder, getClient, getCurrentScope, getGlobalScope, getSelectorRoot, init, makeFetchTransport, manifestsUrl, mountOverlay, overlay, parseDsn, replay, replaysUrl, resolveSelector, resolveUrl, selectorFor, setContext, setReplayRecorder, setSelectorRoot, setTag, setTags, setUser, snapshotElement, withScope };
|