@nominalso/vibe-bridge 0.3.0 → 0.4.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/AGENTS.md +6 -3
- package/README.md +24 -2
- package/dist/index.cjs +111 -1
- package/dist/index.d.cts +102 -2
- package/dist/index.d.ts +102 -2
- package/dist/index.js +101 -1
- package/docs/connect-lifecycle.md +10 -0
- package/docs/getting-started.md +1 -1
- package/llms.txt +2 -2
- package/package.json +4 -1
package/AGENTS.md
CHANGED
|
@@ -4,7 +4,7 @@ Instructions for AI coding agents (Claude Code, Cursor, Copilot, Lovable, etc.)
|
|
|
4
4
|
|
|
5
5
|
## What this package is
|
|
6
6
|
|
|
7
|
-
`@nominalso/vibe-bridge` is the **iframe-side** SDK for a Nominal Vibe App — a standalone browser app (often built with Lovable) embedded in the Nominal platform via a cross-origin `<iframe>`. The app cannot call Nominal APIs directly; it talks to its Nominal host over a typed `postMessage` bridge. This package is that bridge. It is **browser-only** (uses `window`, `postMessage`, `crypto.randomUUID`) and
|
|
7
|
+
`@nominalso/vibe-bridge` is the **iframe-side** SDK for a Nominal Vibe App — a standalone browser app (often built with Lovable) embedded in the Nominal platform via a cross-origin `<iframe>`. The app cannot call Nominal APIs directly; it talks to its Nominal host over a typed `postMessage` bridge. This package is that bridge. It is **browser-only** (uses `window`, `postMessage`, `crypto.randomUUID`) and bundles **`posthog-js`** (its only runtime dependency) for host-enabled in-iframe session recording.
|
|
8
8
|
|
|
9
9
|
The host counterpart is `@nominalso/vibe-host` (used by Nominal's own app — you do not install it in a Vibe App).
|
|
10
10
|
|
|
@@ -37,13 +37,16 @@ bridge.destroy() // on unmount
|
|
|
37
37
|
- `upload(file: File, { entityType, entityId?, onProgress? }): Promise<{ attachmentId, name }>`.
|
|
38
38
|
- `navigate`/`navigateTo`/`refresh` and `ui.setSideNav`/`ui.switchSubsidiary` — drive the host's chrome (router, side nav, subsidiary switcher); `invalidateCache(...)` busts host caches.
|
|
39
39
|
- `reportSubroute(subroute, { replace? })`, `onSubrouteRequest(cb): () => void` — usually unnecessary; standard SPA navigation is auto-tracked.
|
|
40
|
+
- `track(event: string, properties?): void` — capture a custom PostHog event. **No-op unless the host enabled recording** (see below); always safe to call.
|
|
40
41
|
- `destroy()`.
|
|
41
42
|
|
|
43
|
+
**Session recording (PostHog).** The bridge bundles `posthog-js` and, on `connect()`, initializes it **only if the host sent a `posthog` block with `enabled: true`** (cross-origin replay stitched into the host's recording, masking mirroring nom-ui — inputs unmasked, password fields masked — attributed to the same person via `identify`). If the host sends nothing, the bridge initializes nothing — `enabled` is the kill-switch. You configure nothing in the app; just call `track(...)` for custom events. If you serve the app yourself, allow `connect-src https://*.i.posthog.com https://us-assets.i.posthog.com` and `worker-src blob:` in its CSP (no `script-src` entry — posthog is bundled, not from a CDN).
|
|
44
|
+
|
|
42
45
|
A failed operation throws a `BridgeError` (`extends Error`) with a machine-readable `code` (`'RATE_LIMITED'`, `'FILE_TOO_LARGE'`, `'TIMEOUT'`, …); branch on `err.code` rather than the message. A failed Nominal API call throws the `HttpBridgeError` subtype (`code: 'REQUEST_FAILED'`) carrying the HTTP `status` — branch on `err.status` (e.g. `404`).
|
|
43
46
|
|
|
44
|
-
Exported values/types: `VibeAppBridge`, `BridgeError`, `BRIDGE_VERSION`, and types `VibeAppBridgeOptions`, `UploadOptions`, `ContextPayload`, `BridgeSubsidiary`, `UploadResponse`, `UploadProgress`, `RequestRegistry`.
|
|
47
|
+
Exported values/types: `VibeAppBridge`, `BridgeError`, `HttpBridgeError`, `BRIDGE_VERSION`, and types `VibeAppBridgeOptions`, `UploadOptions`, `ContextPayload`, `BridgeSubsidiary`, `PostHogContext`, `UploadResponse`, `UploadProgress`, `RequestRegistry`.
|
|
45
48
|
|
|
46
|
-
`ContextPayload` shape: `{ tenant, subsidiaryId, subsidiaries: { id, displayName }[], user: { id, displayName }, subroute?, lastClosedPeriodSlug?, hostVersion }`.
|
|
49
|
+
`ContextPayload` shape: `{ tenant, subsidiaryId, subsidiaries: { id, displayName }[], user: { id, displayName }, subroute?, lastClosedPeriodSlug?, posthog?: { token, apiHost, distinctId, enabled }, hostVersion }`. The `posthog` block is supplied by the host only when recording is on.
|
|
47
50
|
|
|
48
51
|
The full method/operation list is in `README.md` ("Operation catalog") and in `docs/data-fetching.md`. The detailed wire-up guides are in `docs/`.
|
|
49
52
|
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Iframe-side SDK for building **Nominal Vibe Apps** — standalone web apps (typi
|
|
|
10
10
|
npm install @nominalso/vibe-bridge
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Bundles [`posthog-js`](https://posthog.com/docs/libraries/js) (its only runtime dependency) so the host can enable in-iframe session recording — see [Session recording & custom events](#session-recording--custom-events). Ships ESM + CJS and self-contained TypeScript types.
|
|
14
14
|
|
|
15
15
|
## Quickstart
|
|
16
16
|
|
|
@@ -118,9 +118,31 @@ Uploads a `File` through the host (converts to `ArrayBuffer` first — sandboxed
|
|
|
118
118
|
|
|
119
119
|
Removes listeners, rejects pending requests, and restores patched history methods. Call on unmount.
|
|
120
120
|
|
|
121
|
+
### Session recording & custom events
|
|
122
|
+
|
|
123
|
+
The cross-origin iframe is invisible to the host's own session replay (same-origin policy), so the bridge bundles `posthog-js` and initializes it **itself** — but only when the host opts in. On `connect()`, if the resolved context carries a `posthog` block with `enabled: true`, the bridge:
|
|
124
|
+
|
|
125
|
+
- inits `posthog-js` with `recordCrossOriginIframes: true` (so your replay is **stitched into the host's recording**) and masking that mirrors nom-ui — `maskAllInputs: false` with `maskInputOptions: { password: true }` (inputs unmasked for richer insight, password fields kept masked);
|
|
126
|
+
- calls `identify()` with the host-resolved `distinctId`, so the recording and your events attribute to the **same PostHog person** as nom-ui;
|
|
127
|
+
- if the host also passes its `sessionId` (its `posthog.get_session_id()`), seeds it via `bootstrap.sessionID` so `track()` events share the host's **`$session_id`** and line up with the stitched replay.
|
|
128
|
+
|
|
129
|
+
The host is the single source of truth. **If it sends no `posthog` block (older host, or recording disabled for this env/app), the bridge initializes nothing** — `enabled` is the kill-switch even though `posthog-js` is bundled. You configure nothing in the app.
|
|
130
|
+
|
|
131
|
+
#### `track(event, properties?)`
|
|
132
|
+
|
|
133
|
+
Capture a custom product event. No-op until `connect()` has resolved with PostHog enabled by the host — so it's always safe to call.
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
bridge.track('vibe:fixed-assets:report_exported', { format: 'csv', rows: 128 })
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Namespace your events (e.g. `vibe:<slug>:<action>`) so they stay filterable from the host's own events. Events are sent directly from the iframe's PostHog instance and tie to the same person via `identify()`. By default they carry the iframe instance's `$session_id`; when the host supplies its `sessionId` in the posthog context, the bridge seeds it so events share the host's `$session_id` and correlate with the stitched replay (best-effort — a one-time seed at connect, so a later host-session rotation can diverge).
|
|
140
|
+
|
|
141
|
+
> **App-side CSP.** Whoever serves your app must allow, on the app's own origin, `connect-src https://*.i.posthog.com https://us-assets.i.posthog.com` and `worker-src blob:` (recording runs in a web worker). No `script-src` entry is needed — `posthog-js` is bundled into your app via the bridge, not loaded from a CDN.
|
|
142
|
+
|
|
121
143
|
### Exports
|
|
122
144
|
|
|
123
|
-
`VibeAppBridge`, `BridgeError`, `BRIDGE_VERSION`, and the types `VibeAppBridgeOptions`, `UploadOptions`, `ContextPayload`, `BridgeSubsidiary`, `UploadResponse`, `UploadProgress`, `RequestRegistry`.
|
|
145
|
+
`VibeAppBridge`, `BridgeError`, `HttpBridgeError`, `BRIDGE_VERSION`, and the types `VibeAppBridgeOptions`, `UploadOptions`, `ContextPayload`, `BridgeSubsidiary`, `PostHogContext`, `UploadResponse`, `UploadProgress`, `RequestRegistry`.
|
|
124
146
|
|
|
125
147
|
## Common recipes
|
|
126
148
|
|
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -28,7 +38,7 @@ __export(index_exports, {
|
|
|
28
38
|
module.exports = __toCommonJS(index_exports);
|
|
29
39
|
|
|
30
40
|
// package.json
|
|
31
|
-
var version = "0.
|
|
41
|
+
var version = "0.4.0";
|
|
32
42
|
|
|
33
43
|
// ../protocol-types/dist/index.js
|
|
34
44
|
function normalizeSubroute(subroute) {
|
|
@@ -101,6 +111,9 @@ function isOriginAllowed(origin, allowlist, { allowPatterns }) {
|
|
|
101
111
|
return false;
|
|
102
112
|
}
|
|
103
113
|
|
|
114
|
+
// src/VibeAppBridge.ts
|
|
115
|
+
var import_posthog_js = __toESM(require("posthog-js"), 1);
|
|
116
|
+
|
|
104
117
|
// src/data-methods.ts
|
|
105
118
|
var BridgeDataMethods = class {
|
|
106
119
|
// ---------------------------------------------------------------------------
|
|
@@ -418,6 +431,14 @@ function resolveRequestTimeout(type, requestTimeout) {
|
|
|
418
431
|
}
|
|
419
432
|
|
|
420
433
|
// src/VibeAppBridge.ts
|
|
434
|
+
function resolvePosthog(mod) {
|
|
435
|
+
let candidate = mod;
|
|
436
|
+
while (candidate && typeof candidate.init !== "function" && candidate.default) {
|
|
437
|
+
candidate = candidate.default;
|
|
438
|
+
}
|
|
439
|
+
return candidate;
|
|
440
|
+
}
|
|
441
|
+
var posthog = resolvePosthog(import_posthog_js.default);
|
|
421
442
|
var PARENT_ORIGIN_OVERRIDE_KEY = "__NOM_VIBE_PARENT_ORIGIN__";
|
|
422
443
|
var PREVIEW_HOST_RE = /(?:^|\.)(?:vercel\.app|lovable\.app|lovable\.dev|lovableproject\.com)$|^(?:localhost|127\.0\.0\.1)$/;
|
|
423
444
|
function isPreviewSelfHost(hostname) {
|
|
@@ -481,6 +502,12 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
481
502
|
super();
|
|
482
503
|
this.pendingRequests = /* @__PURE__ */ new Map();
|
|
483
504
|
this.context = null;
|
|
505
|
+
/**
|
|
506
|
+
* Set once {@link initPostHog} has successfully called `posthog.init()`.
|
|
507
|
+
* Guards against a double-init and makes {@link track} a no-op until
|
|
508
|
+
* recording is actually live.
|
|
509
|
+
*/
|
|
510
|
+
this.posthogInitialized = false;
|
|
484
511
|
this.connectPromise = null;
|
|
485
512
|
this.connectPollInterval = null;
|
|
486
513
|
this.connectTimeout = null;
|
|
@@ -611,6 +638,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
611
638
|
console.log(
|
|
612
639
|
`[vibe-bridge] Connected \u2014 bridge: ${version}, host: ${ctx.hostVersion}, tenant: ${ctx.tenant}, user: ${ctx.user.displayName}`
|
|
613
640
|
);
|
|
641
|
+
this.initPostHog(ctx);
|
|
614
642
|
const initialSubroute = ctx.subroute ? normalizeSubroute(ctx.subroute) : null;
|
|
615
643
|
if (initialSubroute && initialSubroute !== "/") {
|
|
616
644
|
this.withSubrouteSuppressed(() => {
|
|
@@ -715,6 +743,34 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
715
743
|
invalidateCache(payload) {
|
|
716
744
|
return this.request("INVALIDATE_CACHE", payload);
|
|
717
745
|
}
|
|
746
|
+
/**
|
|
747
|
+
* Captures a custom product event from inside the Vibe App, attributed to the
|
|
748
|
+
* same PostHog person as the host (via the `distinctId` the host supplied at
|
|
749
|
+
* connect).
|
|
750
|
+
*
|
|
751
|
+
* **No-op until {@link connect} has resolved with PostHog enabled by the
|
|
752
|
+
* host** — if the host didn't send a `posthog` block (older host, or recording
|
|
753
|
+
* disabled for this env/app), nothing is sent. App authors don't import
|
|
754
|
+
* `posthog-js` themselves; this is the supported event surface.
|
|
755
|
+
*
|
|
756
|
+
* The event is sent **directly** from the iframe's own PostHog instance (only
|
|
757
|
+
* the session *recording* is forwarded to the parent), so it carries the
|
|
758
|
+
* iframe instance's `$session_id` rather than the host's — events tie to the
|
|
759
|
+
* same person, but cross-boundary event↔replay timeline correlation is a known
|
|
760
|
+
* limitation.
|
|
761
|
+
*
|
|
762
|
+
* @param event Event name. Namespacing app events (e.g. `'vibe:<slug>:<action>'`)
|
|
763
|
+
* keeps them filterable from the host's own events.
|
|
764
|
+
* @param properties Optional event properties.
|
|
765
|
+
* @example
|
|
766
|
+
* ```ts
|
|
767
|
+
* bridge.track('vibe:fixed-assets:report_exported', { format: 'csv', rows: 128 })
|
|
768
|
+
* ```
|
|
769
|
+
*/
|
|
770
|
+
track(event, properties) {
|
|
771
|
+
if (!this.posthogInitialized) return;
|
|
772
|
+
posthog.capture(event, properties);
|
|
773
|
+
}
|
|
718
774
|
destroy() {
|
|
719
775
|
this.teardownNavigationTracking();
|
|
720
776
|
this.onContextError?.(new BridgeError("DESTROYED", "Bridge destroyed"));
|
|
@@ -725,6 +781,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
725
781
|
}
|
|
726
782
|
this.pendingRequests.clear();
|
|
727
783
|
this.context = null;
|
|
784
|
+
this.posthogInitialized = false;
|
|
728
785
|
this.connectPromise = null;
|
|
729
786
|
this.onContextReceived = null;
|
|
730
787
|
this.onContextError = null;
|
|
@@ -742,6 +799,59 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
742
799
|
}
|
|
743
800
|
this.connectPollIds.clear();
|
|
744
801
|
}
|
|
802
|
+
/**
|
|
803
|
+
* Initializes PostHog session recording + analytics from the host-supplied
|
|
804
|
+
* config, **only when the host opted in** (`ctx.posthog?.enabled`).
|
|
805
|
+
*
|
|
806
|
+
* The host is the single source of truth: when it sends no `posthog` block
|
|
807
|
+
* (older nom-ui, or recording disabled for this env/app) or `enabled: false`,
|
|
808
|
+
* this does nothing — `enabled` is the kill-switch even though `posthog-js` is
|
|
809
|
+
* bundled into the bridge. Cross-origin replay needs `recordCrossOriginIframes`
|
|
810
|
+
* on both sides so the parent receives the iframe's rrweb frames into one
|
|
811
|
+
* stitched recording, and `identify()` ties the iframe's recording + events to
|
|
812
|
+
* the same person as the host. Wrapped in try/catch so a PostHog failure is
|
|
813
|
+
* best-effort and never blocks `connect()`.
|
|
814
|
+
*/
|
|
815
|
+
initPostHog(ctx) {
|
|
816
|
+
const config = ctx.posthog;
|
|
817
|
+
if (!config?.enabled) return;
|
|
818
|
+
if (this.posthogInitialized) return;
|
|
819
|
+
if (typeof window === "undefined") return;
|
|
820
|
+
if (!config.token || !config.apiHost || !config.distinctId) {
|
|
821
|
+
console.warn(
|
|
822
|
+
"[vibe-bridge] PostHog enabled but token/apiHost/distinctId missing \u2014 skipping init."
|
|
823
|
+
);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
try {
|
|
827
|
+
posthog.init(config.token, {
|
|
828
|
+
api_host: config.apiHost,
|
|
829
|
+
// absolute ingestion host, NOT a relative /ingest proxy
|
|
830
|
+
person_profiles: "identified_only",
|
|
831
|
+
capture_pageview: false,
|
|
832
|
+
// the host owns pageviews; don't double-count
|
|
833
|
+
autocapture: false,
|
|
834
|
+
// opt-in events only, via track()
|
|
835
|
+
// Seed the host's session id so track() events share the host's
|
|
836
|
+
// $session_id and line up with the stitched replay. Replay stitching
|
|
837
|
+
// itself does not depend on this (recordCrossOriginIframes handles it).
|
|
838
|
+
...config.sessionId ? { bootstrap: { sessionID: config.sessionId } } : {},
|
|
839
|
+
session_recording: {
|
|
840
|
+
recordCrossOriginIframes: true,
|
|
841
|
+
// REQUIRED so the parent receives our rrweb frames
|
|
842
|
+
// Mirror nom-ui's posture (PostHogProvider.tsx): unmask inputs for
|
|
843
|
+
// richer insight, but keep password fields masked.
|
|
844
|
+
maskAllInputs: false,
|
|
845
|
+
maskInputOptions: { password: true }
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
posthog.identify(config.distinctId);
|
|
849
|
+
this.posthogInitialized = true;
|
|
850
|
+
console.log("[vibe-bridge] PostHog session recording initialized");
|
|
851
|
+
} catch (err) {
|
|
852
|
+
console.warn("[vibe-bridge] PostHog init failed \u2014 continuing without recording.", err);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
745
855
|
/**
|
|
746
856
|
* Monkey-patches `history.pushState` and `history.replaceState` to
|
|
747
857
|
* auto-detect SPA navigation and report it to the host. Called after
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var version = "0.
|
|
1
|
+
var version = "0.4.0";
|
|
2
2
|
|
|
3
3
|
type AccountingAccountDimensionValues = {
|
|
4
4
|
account_id: string;
|
|
@@ -3182,6 +3182,54 @@ interface BridgeSubsidiary {
|
|
|
3182
3182
|
id: number;
|
|
3183
3183
|
displayName: string;
|
|
3184
3184
|
}
|
|
3185
|
+
/**
|
|
3186
|
+
* PostHog configuration the host supplies so the iframe can record a session
|
|
3187
|
+
* replay (stitched into the host's recording via `recordCrossOriginIframes`)
|
|
3188
|
+
* and emit custom events, all attributed to the **same PostHog person** as
|
|
3189
|
+
* nom-ui.
|
|
3190
|
+
*
|
|
3191
|
+
* The host (nom-ui) is the single source of truth for these values — it knows
|
|
3192
|
+
* the project key, the env-aware ingestion host, and the resolved person — and
|
|
3193
|
+
* delivers them over the bridge so the iframe never hardcodes any of it. When
|
|
3194
|
+
* the host omits the {@link ContextPayload.posthog} block entirely (older host,
|
|
3195
|
+
* or recording disabled for this env/app), the bridge initializes nothing.
|
|
3196
|
+
*/
|
|
3197
|
+
interface PostHogContext {
|
|
3198
|
+
/** Project API key — the same PostHog project the host (nom-ui) uses. */
|
|
3199
|
+
token: string;
|
|
3200
|
+
/**
|
|
3201
|
+
* **Absolute** ingestion host, e.g. `"https://us.i.posthog.com"`.
|
|
3202
|
+
*
|
|
3203
|
+
* Must NOT be the host's relative `/ingest` reverse-proxy path: a relative
|
|
3204
|
+
* path resolves against the iframe's *own* (cross-origin) host and 404s.
|
|
3205
|
+
*/
|
|
3206
|
+
apiHost: string;
|
|
3207
|
+
/**
|
|
3208
|
+
* Host-resolved PostHog distinct id. The bridge calls `identify()` with it so
|
|
3209
|
+
* the iframe's recording and events attribute to the same person as the host.
|
|
3210
|
+
*/
|
|
3211
|
+
distinctId: string;
|
|
3212
|
+
/**
|
|
3213
|
+
* **Optional.** The host's current PostHog session id (from
|
|
3214
|
+
* `posthog.get_session_id()`). When provided, the bridge seeds it as the
|
|
3215
|
+
* iframe instance's session via `bootstrap.sessionID`, so custom events from
|
|
3216
|
+
* `bridge.track()` share the **same `$session_id`** as the host — letting them
|
|
3217
|
+
* line up with the stitched session replay on one timeline.
|
|
3218
|
+
*
|
|
3219
|
+
* Not needed for *replay* stitching itself (that works via
|
|
3220
|
+
* `recordCrossOriginIframes` on both sides regardless). Must be a valid UUID
|
|
3221
|
+
* v7 — PostHog session ids already are. It is a one-time seed at connect, so
|
|
3222
|
+
* if the host's session later rotates the two can diverge; alignment is
|
|
3223
|
+
* best-effort for the session in progress.
|
|
3224
|
+
*/
|
|
3225
|
+
sessionId?: string;
|
|
3226
|
+
/**
|
|
3227
|
+
* Master switch, env- and/or app-gated by the host. The bridge only calls
|
|
3228
|
+
* `posthog.init()` when this is `true`; it stays the kill-switch even though
|
|
3229
|
+
* `posthog-js` is bundled into the bridge.
|
|
3230
|
+
*/
|
|
3231
|
+
enabled: boolean;
|
|
3232
|
+
}
|
|
3185
3233
|
interface ContextPayload {
|
|
3186
3234
|
tenant: string;
|
|
3187
3235
|
subsidiaryId: number;
|
|
@@ -3194,6 +3242,13 @@ interface ContextPayload {
|
|
|
3194
3242
|
subroute?: string;
|
|
3195
3243
|
/** Slug of the last closed accounting period, e.g. "mar-2026". */
|
|
3196
3244
|
lastClosedPeriodSlug?: string;
|
|
3245
|
+
/**
|
|
3246
|
+
* PostHog config for in-iframe session recording + custom events. **Absent
|
|
3247
|
+
* when recording is off** (older host, or disabled for this env/app), in which
|
|
3248
|
+
* case the bridge initializes no analytics. Optional so old hosts/bridges stay
|
|
3249
|
+
* compatible.
|
|
3250
|
+
*/
|
|
3251
|
+
posthog?: PostHogContext;
|
|
3197
3252
|
/** Version of the host SDK (@nominalso/vibe-host) running in nom-ui. */
|
|
3198
3253
|
hostVersion: string;
|
|
3199
3254
|
}
|
|
@@ -3731,6 +3786,12 @@ declare class VibeAppBridge extends BridgeDataMethods {
|
|
|
3731
3786
|
private readonly pendingRequests;
|
|
3732
3787
|
private readonly boundHandleMessage;
|
|
3733
3788
|
private context;
|
|
3789
|
+
/**
|
|
3790
|
+
* Set once {@link initPostHog} has successfully called `posthog.init()`.
|
|
3791
|
+
* Guards against a double-init and makes {@link track} a no-op until
|
|
3792
|
+
* recording is actually live.
|
|
3793
|
+
*/
|
|
3794
|
+
private posthogInitialized;
|
|
3734
3795
|
private connectPromise;
|
|
3735
3796
|
private connectPollInterval;
|
|
3736
3797
|
private connectTimeout;
|
|
@@ -3841,8 +3902,47 @@ declare class VibeAppBridge extends BridgeDataMethods {
|
|
|
3841
3902
|
* ```
|
|
3842
3903
|
*/
|
|
3843
3904
|
invalidateCache(payload: InvalidateCachePayload): Promise<InvalidateResult>;
|
|
3905
|
+
/**
|
|
3906
|
+
* Captures a custom product event from inside the Vibe App, attributed to the
|
|
3907
|
+
* same PostHog person as the host (via the `distinctId` the host supplied at
|
|
3908
|
+
* connect).
|
|
3909
|
+
*
|
|
3910
|
+
* **No-op until {@link connect} has resolved with PostHog enabled by the
|
|
3911
|
+
* host** — if the host didn't send a `posthog` block (older host, or recording
|
|
3912
|
+
* disabled for this env/app), nothing is sent. App authors don't import
|
|
3913
|
+
* `posthog-js` themselves; this is the supported event surface.
|
|
3914
|
+
*
|
|
3915
|
+
* The event is sent **directly** from the iframe's own PostHog instance (only
|
|
3916
|
+
* the session *recording* is forwarded to the parent), so it carries the
|
|
3917
|
+
* iframe instance's `$session_id` rather than the host's — events tie to the
|
|
3918
|
+
* same person, but cross-boundary event↔replay timeline correlation is a known
|
|
3919
|
+
* limitation.
|
|
3920
|
+
*
|
|
3921
|
+
* @param event Event name. Namespacing app events (e.g. `'vibe:<slug>:<action>'`)
|
|
3922
|
+
* keeps them filterable from the host's own events.
|
|
3923
|
+
* @param properties Optional event properties.
|
|
3924
|
+
* @example
|
|
3925
|
+
* ```ts
|
|
3926
|
+
* bridge.track('vibe:fixed-assets:report_exported', { format: 'csv', rows: 128 })
|
|
3927
|
+
* ```
|
|
3928
|
+
*/
|
|
3929
|
+
track(event: string, properties?: Record<string, unknown>): void;
|
|
3844
3930
|
destroy(): void;
|
|
3845
3931
|
private stopConnectPolling;
|
|
3932
|
+
/**
|
|
3933
|
+
* Initializes PostHog session recording + analytics from the host-supplied
|
|
3934
|
+
* config, **only when the host opted in** (`ctx.posthog?.enabled`).
|
|
3935
|
+
*
|
|
3936
|
+
* The host is the single source of truth: when it sends no `posthog` block
|
|
3937
|
+
* (older nom-ui, or recording disabled for this env/app) or `enabled: false`,
|
|
3938
|
+
* this does nothing — `enabled` is the kill-switch even though `posthog-js` is
|
|
3939
|
+
* bundled into the bridge. Cross-origin replay needs `recordCrossOriginIframes`
|
|
3940
|
+
* on both sides so the parent receives the iframe's rrweb frames into one
|
|
3941
|
+
* stitched recording, and `identify()` ties the iframe's recording + events to
|
|
3942
|
+
* the same person as the host. Wrapped in try/catch so a PostHog failure is
|
|
3943
|
+
* best-effort and never blocks `connect()`.
|
|
3944
|
+
*/
|
|
3945
|
+
private initPostHog;
|
|
3846
3946
|
/**
|
|
3847
3947
|
* Monkey-patches `history.pushState` and `history.replaceState` to
|
|
3848
3948
|
* auto-detect SPA navigation and report it to the host. Called after
|
|
@@ -3878,4 +3978,4 @@ declare class VibeAppBridge extends BridgeDataMethods {
|
|
|
3878
3978
|
private handleProgress;
|
|
3879
3979
|
}
|
|
3880
3980
|
|
|
3881
|
-
export { version as BRIDGE_VERSION, BridgeError, type BridgeSubsidiary, type ContextPayload, HttpBridgeError, type InvalidateCachePayload, type InvalidateResult, type NavigateAction, type RequestRegistry, type SetSideNavPayload, type UploadOptions, type UploadProgress, type UploadResponse, VibeAppBridge, type VibeAppBridgeOptions };
|
|
3981
|
+
export { version as BRIDGE_VERSION, BridgeError, type BridgeSubsidiary, type ContextPayload, HttpBridgeError, type InvalidateCachePayload, type InvalidateResult, type NavigateAction, type PostHogContext, type RequestRegistry, type SetSideNavPayload, type UploadOptions, type UploadProgress, type UploadResponse, VibeAppBridge, type VibeAppBridgeOptions };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var version = "0.
|
|
1
|
+
var version = "0.4.0";
|
|
2
2
|
|
|
3
3
|
type AccountingAccountDimensionValues = {
|
|
4
4
|
account_id: string;
|
|
@@ -3182,6 +3182,54 @@ interface BridgeSubsidiary {
|
|
|
3182
3182
|
id: number;
|
|
3183
3183
|
displayName: string;
|
|
3184
3184
|
}
|
|
3185
|
+
/**
|
|
3186
|
+
* PostHog configuration the host supplies so the iframe can record a session
|
|
3187
|
+
* replay (stitched into the host's recording via `recordCrossOriginIframes`)
|
|
3188
|
+
* and emit custom events, all attributed to the **same PostHog person** as
|
|
3189
|
+
* nom-ui.
|
|
3190
|
+
*
|
|
3191
|
+
* The host (nom-ui) is the single source of truth for these values — it knows
|
|
3192
|
+
* the project key, the env-aware ingestion host, and the resolved person — and
|
|
3193
|
+
* delivers them over the bridge so the iframe never hardcodes any of it. When
|
|
3194
|
+
* the host omits the {@link ContextPayload.posthog} block entirely (older host,
|
|
3195
|
+
* or recording disabled for this env/app), the bridge initializes nothing.
|
|
3196
|
+
*/
|
|
3197
|
+
interface PostHogContext {
|
|
3198
|
+
/** Project API key — the same PostHog project the host (nom-ui) uses. */
|
|
3199
|
+
token: string;
|
|
3200
|
+
/**
|
|
3201
|
+
* **Absolute** ingestion host, e.g. `"https://us.i.posthog.com"`.
|
|
3202
|
+
*
|
|
3203
|
+
* Must NOT be the host's relative `/ingest` reverse-proxy path: a relative
|
|
3204
|
+
* path resolves against the iframe's *own* (cross-origin) host and 404s.
|
|
3205
|
+
*/
|
|
3206
|
+
apiHost: string;
|
|
3207
|
+
/**
|
|
3208
|
+
* Host-resolved PostHog distinct id. The bridge calls `identify()` with it so
|
|
3209
|
+
* the iframe's recording and events attribute to the same person as the host.
|
|
3210
|
+
*/
|
|
3211
|
+
distinctId: string;
|
|
3212
|
+
/**
|
|
3213
|
+
* **Optional.** The host's current PostHog session id (from
|
|
3214
|
+
* `posthog.get_session_id()`). When provided, the bridge seeds it as the
|
|
3215
|
+
* iframe instance's session via `bootstrap.sessionID`, so custom events from
|
|
3216
|
+
* `bridge.track()` share the **same `$session_id`** as the host — letting them
|
|
3217
|
+
* line up with the stitched session replay on one timeline.
|
|
3218
|
+
*
|
|
3219
|
+
* Not needed for *replay* stitching itself (that works via
|
|
3220
|
+
* `recordCrossOriginIframes` on both sides regardless). Must be a valid UUID
|
|
3221
|
+
* v7 — PostHog session ids already are. It is a one-time seed at connect, so
|
|
3222
|
+
* if the host's session later rotates the two can diverge; alignment is
|
|
3223
|
+
* best-effort for the session in progress.
|
|
3224
|
+
*/
|
|
3225
|
+
sessionId?: string;
|
|
3226
|
+
/**
|
|
3227
|
+
* Master switch, env- and/or app-gated by the host. The bridge only calls
|
|
3228
|
+
* `posthog.init()` when this is `true`; it stays the kill-switch even though
|
|
3229
|
+
* `posthog-js` is bundled into the bridge.
|
|
3230
|
+
*/
|
|
3231
|
+
enabled: boolean;
|
|
3232
|
+
}
|
|
3185
3233
|
interface ContextPayload {
|
|
3186
3234
|
tenant: string;
|
|
3187
3235
|
subsidiaryId: number;
|
|
@@ -3194,6 +3242,13 @@ interface ContextPayload {
|
|
|
3194
3242
|
subroute?: string;
|
|
3195
3243
|
/** Slug of the last closed accounting period, e.g. "mar-2026". */
|
|
3196
3244
|
lastClosedPeriodSlug?: string;
|
|
3245
|
+
/**
|
|
3246
|
+
* PostHog config for in-iframe session recording + custom events. **Absent
|
|
3247
|
+
* when recording is off** (older host, or disabled for this env/app), in which
|
|
3248
|
+
* case the bridge initializes no analytics. Optional so old hosts/bridges stay
|
|
3249
|
+
* compatible.
|
|
3250
|
+
*/
|
|
3251
|
+
posthog?: PostHogContext;
|
|
3197
3252
|
/** Version of the host SDK (@nominalso/vibe-host) running in nom-ui. */
|
|
3198
3253
|
hostVersion: string;
|
|
3199
3254
|
}
|
|
@@ -3731,6 +3786,12 @@ declare class VibeAppBridge extends BridgeDataMethods {
|
|
|
3731
3786
|
private readonly pendingRequests;
|
|
3732
3787
|
private readonly boundHandleMessage;
|
|
3733
3788
|
private context;
|
|
3789
|
+
/**
|
|
3790
|
+
* Set once {@link initPostHog} has successfully called `posthog.init()`.
|
|
3791
|
+
* Guards against a double-init and makes {@link track} a no-op until
|
|
3792
|
+
* recording is actually live.
|
|
3793
|
+
*/
|
|
3794
|
+
private posthogInitialized;
|
|
3734
3795
|
private connectPromise;
|
|
3735
3796
|
private connectPollInterval;
|
|
3736
3797
|
private connectTimeout;
|
|
@@ -3841,8 +3902,47 @@ declare class VibeAppBridge extends BridgeDataMethods {
|
|
|
3841
3902
|
* ```
|
|
3842
3903
|
*/
|
|
3843
3904
|
invalidateCache(payload: InvalidateCachePayload): Promise<InvalidateResult>;
|
|
3905
|
+
/**
|
|
3906
|
+
* Captures a custom product event from inside the Vibe App, attributed to the
|
|
3907
|
+
* same PostHog person as the host (via the `distinctId` the host supplied at
|
|
3908
|
+
* connect).
|
|
3909
|
+
*
|
|
3910
|
+
* **No-op until {@link connect} has resolved with PostHog enabled by the
|
|
3911
|
+
* host** — if the host didn't send a `posthog` block (older host, or recording
|
|
3912
|
+
* disabled for this env/app), nothing is sent. App authors don't import
|
|
3913
|
+
* `posthog-js` themselves; this is the supported event surface.
|
|
3914
|
+
*
|
|
3915
|
+
* The event is sent **directly** from the iframe's own PostHog instance (only
|
|
3916
|
+
* the session *recording* is forwarded to the parent), so it carries the
|
|
3917
|
+
* iframe instance's `$session_id` rather than the host's — events tie to the
|
|
3918
|
+
* same person, but cross-boundary event↔replay timeline correlation is a known
|
|
3919
|
+
* limitation.
|
|
3920
|
+
*
|
|
3921
|
+
* @param event Event name. Namespacing app events (e.g. `'vibe:<slug>:<action>'`)
|
|
3922
|
+
* keeps them filterable from the host's own events.
|
|
3923
|
+
* @param properties Optional event properties.
|
|
3924
|
+
* @example
|
|
3925
|
+
* ```ts
|
|
3926
|
+
* bridge.track('vibe:fixed-assets:report_exported', { format: 'csv', rows: 128 })
|
|
3927
|
+
* ```
|
|
3928
|
+
*/
|
|
3929
|
+
track(event: string, properties?: Record<string, unknown>): void;
|
|
3844
3930
|
destroy(): void;
|
|
3845
3931
|
private stopConnectPolling;
|
|
3932
|
+
/**
|
|
3933
|
+
* Initializes PostHog session recording + analytics from the host-supplied
|
|
3934
|
+
* config, **only when the host opted in** (`ctx.posthog?.enabled`).
|
|
3935
|
+
*
|
|
3936
|
+
* The host is the single source of truth: when it sends no `posthog` block
|
|
3937
|
+
* (older nom-ui, or recording disabled for this env/app) or `enabled: false`,
|
|
3938
|
+
* this does nothing — `enabled` is the kill-switch even though `posthog-js` is
|
|
3939
|
+
* bundled into the bridge. Cross-origin replay needs `recordCrossOriginIframes`
|
|
3940
|
+
* on both sides so the parent receives the iframe's rrweb frames into one
|
|
3941
|
+
* stitched recording, and `identify()` ties the iframe's recording + events to
|
|
3942
|
+
* the same person as the host. Wrapped in try/catch so a PostHog failure is
|
|
3943
|
+
* best-effort and never blocks `connect()`.
|
|
3944
|
+
*/
|
|
3945
|
+
private initPostHog;
|
|
3846
3946
|
/**
|
|
3847
3947
|
* Monkey-patches `history.pushState` and `history.replaceState` to
|
|
3848
3948
|
* auto-detect SPA navigation and report it to the host. Called after
|
|
@@ -3878,4 +3978,4 @@ declare class VibeAppBridge extends BridgeDataMethods {
|
|
|
3878
3978
|
private handleProgress;
|
|
3879
3979
|
}
|
|
3880
3980
|
|
|
3881
|
-
export { version as BRIDGE_VERSION, BridgeError, type BridgeSubsidiary, type ContextPayload, HttpBridgeError, type InvalidateCachePayload, type InvalidateResult, type NavigateAction, type RequestRegistry, type SetSideNavPayload, type UploadOptions, type UploadProgress, type UploadResponse, VibeAppBridge, type VibeAppBridgeOptions };
|
|
3981
|
+
export { version as BRIDGE_VERSION, BridgeError, type BridgeSubsidiary, type ContextPayload, HttpBridgeError, type InvalidateCachePayload, type InvalidateResult, type NavigateAction, type PostHogContext, type RequestRegistry, type SetSideNavPayload, type UploadOptions, type UploadProgress, type UploadResponse, VibeAppBridge, type VibeAppBridgeOptions };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// package.json
|
|
2
|
-
var version = "0.
|
|
2
|
+
var version = "0.4.0";
|
|
3
3
|
|
|
4
4
|
// ../protocol-types/dist/index.js
|
|
5
5
|
function normalizeSubroute(subroute) {
|
|
@@ -72,6 +72,9 @@ function isOriginAllowed(origin, allowlist, { allowPatterns }) {
|
|
|
72
72
|
return false;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// src/VibeAppBridge.ts
|
|
76
|
+
import posthogDefault from "posthog-js";
|
|
77
|
+
|
|
75
78
|
// src/data-methods.ts
|
|
76
79
|
var BridgeDataMethods = class {
|
|
77
80
|
// ---------------------------------------------------------------------------
|
|
@@ -389,6 +392,14 @@ function resolveRequestTimeout(type, requestTimeout) {
|
|
|
389
392
|
}
|
|
390
393
|
|
|
391
394
|
// src/VibeAppBridge.ts
|
|
395
|
+
function resolvePosthog(mod) {
|
|
396
|
+
let candidate = mod;
|
|
397
|
+
while (candidate && typeof candidate.init !== "function" && candidate.default) {
|
|
398
|
+
candidate = candidate.default;
|
|
399
|
+
}
|
|
400
|
+
return candidate;
|
|
401
|
+
}
|
|
402
|
+
var posthog = resolvePosthog(posthogDefault);
|
|
392
403
|
var PARENT_ORIGIN_OVERRIDE_KEY = "__NOM_VIBE_PARENT_ORIGIN__";
|
|
393
404
|
var PREVIEW_HOST_RE = /(?:^|\.)(?:vercel\.app|lovable\.app|lovable\.dev|lovableproject\.com)$|^(?:localhost|127\.0\.0\.1)$/;
|
|
394
405
|
function isPreviewSelfHost(hostname) {
|
|
@@ -452,6 +463,12 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
452
463
|
super();
|
|
453
464
|
this.pendingRequests = /* @__PURE__ */ new Map();
|
|
454
465
|
this.context = null;
|
|
466
|
+
/**
|
|
467
|
+
* Set once {@link initPostHog} has successfully called `posthog.init()`.
|
|
468
|
+
* Guards against a double-init and makes {@link track} a no-op until
|
|
469
|
+
* recording is actually live.
|
|
470
|
+
*/
|
|
471
|
+
this.posthogInitialized = false;
|
|
455
472
|
this.connectPromise = null;
|
|
456
473
|
this.connectPollInterval = null;
|
|
457
474
|
this.connectTimeout = null;
|
|
@@ -582,6 +599,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
582
599
|
console.log(
|
|
583
600
|
`[vibe-bridge] Connected \u2014 bridge: ${version}, host: ${ctx.hostVersion}, tenant: ${ctx.tenant}, user: ${ctx.user.displayName}`
|
|
584
601
|
);
|
|
602
|
+
this.initPostHog(ctx);
|
|
585
603
|
const initialSubroute = ctx.subroute ? normalizeSubroute(ctx.subroute) : null;
|
|
586
604
|
if (initialSubroute && initialSubroute !== "/") {
|
|
587
605
|
this.withSubrouteSuppressed(() => {
|
|
@@ -686,6 +704,34 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
686
704
|
invalidateCache(payload) {
|
|
687
705
|
return this.request("INVALIDATE_CACHE", payload);
|
|
688
706
|
}
|
|
707
|
+
/**
|
|
708
|
+
* Captures a custom product event from inside the Vibe App, attributed to the
|
|
709
|
+
* same PostHog person as the host (via the `distinctId` the host supplied at
|
|
710
|
+
* connect).
|
|
711
|
+
*
|
|
712
|
+
* **No-op until {@link connect} has resolved with PostHog enabled by the
|
|
713
|
+
* host** — if the host didn't send a `posthog` block (older host, or recording
|
|
714
|
+
* disabled for this env/app), nothing is sent. App authors don't import
|
|
715
|
+
* `posthog-js` themselves; this is the supported event surface.
|
|
716
|
+
*
|
|
717
|
+
* The event is sent **directly** from the iframe's own PostHog instance (only
|
|
718
|
+
* the session *recording* is forwarded to the parent), so it carries the
|
|
719
|
+
* iframe instance's `$session_id` rather than the host's — events tie to the
|
|
720
|
+
* same person, but cross-boundary event↔replay timeline correlation is a known
|
|
721
|
+
* limitation.
|
|
722
|
+
*
|
|
723
|
+
* @param event Event name. Namespacing app events (e.g. `'vibe:<slug>:<action>'`)
|
|
724
|
+
* keeps them filterable from the host's own events.
|
|
725
|
+
* @param properties Optional event properties.
|
|
726
|
+
* @example
|
|
727
|
+
* ```ts
|
|
728
|
+
* bridge.track('vibe:fixed-assets:report_exported', { format: 'csv', rows: 128 })
|
|
729
|
+
* ```
|
|
730
|
+
*/
|
|
731
|
+
track(event, properties) {
|
|
732
|
+
if (!this.posthogInitialized) return;
|
|
733
|
+
posthog.capture(event, properties);
|
|
734
|
+
}
|
|
689
735
|
destroy() {
|
|
690
736
|
this.teardownNavigationTracking();
|
|
691
737
|
this.onContextError?.(new BridgeError("DESTROYED", "Bridge destroyed"));
|
|
@@ -696,6 +742,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
696
742
|
}
|
|
697
743
|
this.pendingRequests.clear();
|
|
698
744
|
this.context = null;
|
|
745
|
+
this.posthogInitialized = false;
|
|
699
746
|
this.connectPromise = null;
|
|
700
747
|
this.onContextReceived = null;
|
|
701
748
|
this.onContextError = null;
|
|
@@ -713,6 +760,59 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
713
760
|
}
|
|
714
761
|
this.connectPollIds.clear();
|
|
715
762
|
}
|
|
763
|
+
/**
|
|
764
|
+
* Initializes PostHog session recording + analytics from the host-supplied
|
|
765
|
+
* config, **only when the host opted in** (`ctx.posthog?.enabled`).
|
|
766
|
+
*
|
|
767
|
+
* The host is the single source of truth: when it sends no `posthog` block
|
|
768
|
+
* (older nom-ui, or recording disabled for this env/app) or `enabled: false`,
|
|
769
|
+
* this does nothing — `enabled` is the kill-switch even though `posthog-js` is
|
|
770
|
+
* bundled into the bridge. Cross-origin replay needs `recordCrossOriginIframes`
|
|
771
|
+
* on both sides so the parent receives the iframe's rrweb frames into one
|
|
772
|
+
* stitched recording, and `identify()` ties the iframe's recording + events to
|
|
773
|
+
* the same person as the host. Wrapped in try/catch so a PostHog failure is
|
|
774
|
+
* best-effort and never blocks `connect()`.
|
|
775
|
+
*/
|
|
776
|
+
initPostHog(ctx) {
|
|
777
|
+
const config = ctx.posthog;
|
|
778
|
+
if (!config?.enabled) return;
|
|
779
|
+
if (this.posthogInitialized) return;
|
|
780
|
+
if (typeof window === "undefined") return;
|
|
781
|
+
if (!config.token || !config.apiHost || !config.distinctId) {
|
|
782
|
+
console.warn(
|
|
783
|
+
"[vibe-bridge] PostHog enabled but token/apiHost/distinctId missing \u2014 skipping init."
|
|
784
|
+
);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
try {
|
|
788
|
+
posthog.init(config.token, {
|
|
789
|
+
api_host: config.apiHost,
|
|
790
|
+
// absolute ingestion host, NOT a relative /ingest proxy
|
|
791
|
+
person_profiles: "identified_only",
|
|
792
|
+
capture_pageview: false,
|
|
793
|
+
// the host owns pageviews; don't double-count
|
|
794
|
+
autocapture: false,
|
|
795
|
+
// opt-in events only, via track()
|
|
796
|
+
// Seed the host's session id so track() events share the host's
|
|
797
|
+
// $session_id and line up with the stitched replay. Replay stitching
|
|
798
|
+
// itself does not depend on this (recordCrossOriginIframes handles it).
|
|
799
|
+
...config.sessionId ? { bootstrap: { sessionID: config.sessionId } } : {},
|
|
800
|
+
session_recording: {
|
|
801
|
+
recordCrossOriginIframes: true,
|
|
802
|
+
// REQUIRED so the parent receives our rrweb frames
|
|
803
|
+
// Mirror nom-ui's posture (PostHogProvider.tsx): unmask inputs for
|
|
804
|
+
// richer insight, but keep password fields masked.
|
|
805
|
+
maskAllInputs: false,
|
|
806
|
+
maskInputOptions: { password: true }
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
posthog.identify(config.distinctId);
|
|
810
|
+
this.posthogInitialized = true;
|
|
811
|
+
console.log("[vibe-bridge] PostHog session recording initialized");
|
|
812
|
+
} catch (err) {
|
|
813
|
+
console.warn("[vibe-bridge] PostHog init failed \u2014 continuing without recording.", err);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
716
816
|
/**
|
|
717
817
|
* Monkey-patches `history.pushState` and `history.replaceState` to
|
|
718
818
|
* auto-detect SPA navigation and report it to the host. Called after
|
|
@@ -20,10 +20,20 @@ interface ContextPayload {
|
|
|
20
20
|
user: { id: string; displayName: string }
|
|
21
21
|
subroute?: string // initial deep-link path, if any
|
|
22
22
|
lastClosedPeriodSlug?: string // e.g. "mar-2026"
|
|
23
|
+
posthog?: {
|
|
24
|
+
// present only when the host enables in-iframe recording
|
|
25
|
+
token: string
|
|
26
|
+
apiHost: string // absolute ingestion host, e.g. "https://us.i.posthog.com"
|
|
27
|
+
distinctId: string // host-resolved person id (so events tie to the same person)
|
|
28
|
+
sessionId?: string // host's posthog.get_session_id(); seeds bootstrap so track() events share the host session
|
|
29
|
+
enabled: boolean // master switch; false/absent → bridge initializes nothing
|
|
30
|
+
}
|
|
23
31
|
hostVersion: string // version of @nominalso/vibe-host running in the host
|
|
24
32
|
}
|
|
25
33
|
```
|
|
26
34
|
|
|
35
|
+
When the resolved context carries `posthog.enabled === true`, the bridge auto-initializes `posthog-js` for cross-origin session recording (stitched into the host's recording) and exposes `bridge.track(event, properties?)` for custom events — see [Session recording & custom events](../README.md#session-recording--custom-events). If the host omits the `posthog` block, recording stays off.
|
|
36
|
+
|
|
27
37
|
## How it works
|
|
28
38
|
|
|
29
39
|
The host pushes context to the iframe on load, but the iframe's listener may not be ready when that push arrives. `connect()` solves the race:
|
package/docs/getting-started.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
npm install @nominalso/vibe-bridge
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Bundles `posthog-js` (its only runtime dependency) for host-enabled in-iframe session recording. Ships ESM + CJS with self-contained TypeScript types.
|
|
12
12
|
|
|
13
13
|
## Minimal app
|
|
14
14
|
|
package/llms.txt
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @nominalso/vibe-bridge
|
|
2
2
|
|
|
3
|
-
> Iframe-side TypeScript SDK for building Nominal Vibe Apps — standalone browser apps (often built with Lovable) embedded in the Nominal platform via a cross-origin iframe. Connects the app to its Nominal host over a typed postMessage protocol: read Nominal data, submit Close-Management task outputs, upload files,
|
|
3
|
+
> Iframe-side TypeScript SDK for building Nominal Vibe Apps — standalone browser apps (often built with Lovable) embedded in the Nominal platform via a cross-origin iframe. Connects the app to its Nominal host over a typed postMessage protocol: read Nominal data, submit Close-Management task outputs, upload files, keep deep-link routing in sync, and (when the host enables it) record the in-iframe session to PostHog. Browser-only; bundles posthog-js as its only runtime dependency.
|
|
4
4
|
|
|
5
|
-
The integration is always the same: `new VibeAppBridge()` (no config — `parentOrigin` is optional and the bridge auto-detects the embedding Nominal host; pass it only to pin explicitly or to allow preview origins), `await bridge.connect()` once (it returns the tenant/user `ContextPayload`), then call any of the ~46 typed data methods, `upload(file, options)`, or `postTaskOutput(...)`. Tear down with `bridge.destroy()`. The only write path into Nominal is `postTaskOutput`.
|
|
5
|
+
The integration is always the same: `new VibeAppBridge()` (no config — `parentOrigin` is optional and the bridge auto-detects the embedding Nominal host; pass it only to pin explicitly or to allow preview origins), `await bridge.connect()` once (it returns the tenant/user `ContextPayload`), then call any of the ~46 typed data methods, `upload(file, options)`, or `postTaskOutput(...)`. Tear down with `bridge.destroy()`. The only write path into Nominal is `postTaskOutput`. Session recording auto-initializes on `connect()` only when the host sends a `posthog` block with `enabled: true`; emit custom events with `bridge.track(event, properties?)` (a no-op until then).
|
|
6
6
|
|
|
7
7
|
## Docs
|
|
8
8
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nominalso/vibe-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Iframe-side SDK for building Nominal Vibe Apps — connects an embedded app to its Nominal host over a typed postMessage bridge (context, data fetch, file upload, subroute deep-linking).",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://github.com/nominalso/vibe-apps-sdk#readme",
|
|
@@ -33,6 +33,9 @@
|
|
|
33
33
|
"registry": "https://registry.npmjs.org/",
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"posthog-js": "^1.393.5"
|
|
38
|
+
},
|
|
36
39
|
"devDependencies": {
|
|
37
40
|
"@nominalso/vibe-api-types": "0.0.1",
|
|
38
41
|
"@nominalso/vibe-protocol-types": "0.0.1"
|