@lotics/app-sdk 0.77.4 → 0.78.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/analytics.d.ts +0 -6
- package/dist/src/analytics.js +19 -42
- package/dist/src/comments.js +0 -4
- package/dist/src/hooks.js +4 -21
- package/docs/ai.md +3 -3
- package/docs/runtime.md +19 -27
- package/package.json +1 -1
package/dist/src/analytics.d.ts
CHANGED
|
@@ -1,7 +1 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Capture an app event. Buffers until PostHog is ready, then emits directly;
|
|
3
|
-
* no-ops permanently once tracking is known to be off, so the hooks can call it
|
|
4
|
-
* unconditionally.
|
|
5
|
-
*/
|
|
6
|
-
export declare function captureAppEvent(event: string, props?: Record<string, unknown>): void;
|
|
7
1
|
export declare function bootstrapAnalytics(): Promise<void>;
|
package/dist/src/analytics.js
CHANGED
|
@@ -9,12 +9,20 @@
|
|
|
9
9
|
* values — and Điều 3.2 of the service contract warrants that our telemetry
|
|
10
10
|
* carries no record or document content. The flags below are what enforce
|
|
11
11
|
* that; the project-level setting is NOT a backstop, since it is server-side
|
|
12
|
-
* and fails open when the config fetch fails.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* and
|
|
17
|
-
*
|
|
12
|
+
* and fails open when the config fetch fails.
|
|
13
|
+
*
|
|
14
|
+
* **What earns an event:** who KNOWS the fact. An effect the platform performs
|
|
15
|
+
* or persists is the platform's — a comment, an upload, a workflow or agent run
|
|
16
|
+
* all leave rows, and a client copy would duplicate one AND keep arriving from
|
|
17
|
+
* apps pinned to an old SDK long after the code is deleted. A FAILURE is not a
|
|
18
|
+
* gesture either; it is system telemetry, and this bundle has no log sink to
|
|
19
|
+
* carry it, so it is not an event here by default.
|
|
20
|
+
*
|
|
21
|
+
* That leaves exactly one: `app_opened`. Apps are a separate origin the
|
|
22
|
+
* product's own analytics cannot see, and the app-host gate log records gate
|
|
23
|
+
* DECISIONS rather than routine serves, so nothing else counts an app being
|
|
24
|
+
* used. Everything a run does is already in `app_agent_runs` — including a
|
|
25
|
+
* user's Stop, which stamps `cancel_requested_at`.
|
|
18
26
|
*
|
|
19
27
|
* Every event is tagged with app identity and rolls up under the existing
|
|
20
28
|
* `organization` group; embedded apps `identify` the member the host passes
|
|
@@ -33,34 +41,6 @@ import { hasMockFlag } from "./mock.js";
|
|
|
33
41
|
const POSTHOG_KEY = "phc_N1nyqSRdo9XMK3DODxrxX2Y9jG3dppybruOuMznbz62";
|
|
34
42
|
const POSTHOG_HOST = "https://us.i.posthog.com";
|
|
35
43
|
const APP_HOST_SUFFIX = ".lotics.app";
|
|
36
|
-
let loaded = false;
|
|
37
|
-
let disabled = false;
|
|
38
|
-
/**
|
|
39
|
-
* Events fired before init completes (a hook can settle during the `context`
|
|
40
|
-
* round-trip). Drained on init; capped so a never-initializing session can't
|
|
41
|
-
* grow it unbounded.
|
|
42
|
-
*/
|
|
43
|
-
const preInitQueue = [];
|
|
44
|
-
const MAX_QUEUE = 100;
|
|
45
|
-
function disable() {
|
|
46
|
-
disabled = true;
|
|
47
|
-
preInitQueue.length = 0;
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Capture an app event. Buffers until PostHog is ready, then emits directly;
|
|
51
|
-
* no-ops permanently once tracking is known to be off, so the hooks can call it
|
|
52
|
-
* unconditionally.
|
|
53
|
-
*/
|
|
54
|
-
export function captureAppEvent(event, props) {
|
|
55
|
-
if (disabled)
|
|
56
|
-
return;
|
|
57
|
-
if (loaded) {
|
|
58
|
-
posthog.capture(event, props);
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
if (preInitQueue.length < MAX_QUEUE)
|
|
62
|
-
preInitQueue.push({ event, props });
|
|
63
|
-
}
|
|
64
44
|
function onDeployedAppHost() {
|
|
65
45
|
try {
|
|
66
46
|
return window.location.hostname.endsWith(APP_HOST_SUFFIX);
|
|
@@ -73,17 +53,14 @@ export async function bootstrapAnalytics() {
|
|
|
73
53
|
// Only the deployed app host tracks: a design-time/screenshot load
|
|
74
54
|
// (?__mock=1) and `lotics app dev` (localhost) emit nothing. This replaces
|
|
75
55
|
// the old "no key off-prod" gate now that the key is hardcoded.
|
|
76
|
-
if (hasMockFlag() || !onDeployedAppHost())
|
|
77
|
-
disable();
|
|
56
|
+
if (hasMockFlag() || !onDeployedAppHost())
|
|
78
57
|
return;
|
|
79
|
-
}
|
|
80
58
|
let ctx;
|
|
81
59
|
try {
|
|
82
60
|
ctx = await rpc("context", {});
|
|
83
61
|
}
|
|
84
62
|
catch {
|
|
85
63
|
// Best-effort: a context-resolution failure must never break the app.
|
|
86
|
-
disable();
|
|
87
64
|
return;
|
|
88
65
|
}
|
|
89
66
|
posthog.init(POSTHOG_KEY, {
|
|
@@ -113,9 +90,12 @@ export async function bootstrapAnalytics() {
|
|
|
113
90
|
// Tag + capture from `loaded` (once PostHog has initialized) — the robust
|
|
114
91
|
// point to register super-properties and emit the first event.
|
|
115
92
|
loaded: (ph) => {
|
|
93
|
+
// Ids only. A super-property rides every event and every `$exception`
|
|
94
|
+
// the session sends, and an app's NAME is the customer's own words —
|
|
95
|
+
// routinely a counterparty — which Điều 3.2 keeps out of telemetry. The
|
|
96
|
+
// id resolves to the name for anyone entitled to it.
|
|
116
97
|
ph.register({
|
|
117
98
|
app_id: ctx.app_id,
|
|
118
|
-
app_name: ctx.app_name,
|
|
119
99
|
workspace_id: ctx.workspace_id,
|
|
120
100
|
organization_id: ctx.organization_id,
|
|
121
101
|
});
|
|
@@ -127,10 +107,7 @@ export async function bootstrapAnalytics() {
|
|
|
127
107
|
// visitors stay anonymous (member_id null).
|
|
128
108
|
if (ctx.member_id)
|
|
129
109
|
ph.identify(ctx.member_id);
|
|
130
|
-
loaded = true;
|
|
131
110
|
ph.capture("app_opened");
|
|
132
|
-
for (const e of preInitQueue.splice(0))
|
|
133
|
-
ph.capture(e.event, e.props);
|
|
134
111
|
},
|
|
135
112
|
});
|
|
136
113
|
}
|
package/dist/src/comments.js
CHANGED
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
import { useCallback } from "react";
|
|
26
26
|
import useSWR from "swr";
|
|
27
27
|
import { rpc } from "./rpc.js";
|
|
28
|
-
import { captureAppEvent } from "./analytics.js";
|
|
29
28
|
import { useAppContext } from "./viewer.js";
|
|
30
29
|
/** The reduced storage shape the update endpoint accepts for `files`. */
|
|
31
30
|
function toStorageFiles(files) {
|
|
@@ -101,7 +100,6 @@ export function useComments(args) {
|
|
|
101
100
|
revalidate: false,
|
|
102
101
|
populateCache: true,
|
|
103
102
|
});
|
|
104
|
-
captureAppEvent("app_comment_created", { has_files: fileIds.length > 0 });
|
|
105
103
|
}, [available, memberId, record_id, swr]);
|
|
106
104
|
const updateComment = useCallback(async (id, input) => {
|
|
107
105
|
if (!available)
|
|
@@ -129,7 +127,6 @@ export function useComments(args) {
|
|
|
129
127
|
revalidate: false,
|
|
130
128
|
populateCache: true,
|
|
131
129
|
});
|
|
132
|
-
captureAppEvent("app_comment_updated", {});
|
|
133
130
|
}, [available, record_id, swr]);
|
|
134
131
|
const deleteComment = useCallback(async (id) => {
|
|
135
132
|
if (!available)
|
|
@@ -145,7 +142,6 @@ export function useComments(args) {
|
|
|
145
142
|
revalidate: false,
|
|
146
143
|
populateCache: true,
|
|
147
144
|
});
|
|
148
|
-
captureAppEvent("app_comment_deleted", {});
|
|
149
145
|
}, [available, record_id, swr]);
|
|
150
146
|
return {
|
|
151
147
|
comments,
|
package/dist/src/hooks.js
CHANGED
|
@@ -22,7 +22,6 @@ import useSWRInfinite from "swr/infinite";
|
|
|
22
22
|
import { rpc, rpcAgentRun, rpcAgentRunContinue, postHostNotification, subscribeHostRefetch, } from "./rpc.js";
|
|
23
23
|
import { initialAgentRunState, reduceAgentChunk, parseSseChunks, adoptSettledRun, pendingInteractiveCall, buildChoiceOutput, applyInteractiveAnswer, landingOf, proseOf, ABORTED, } from "./agent_stream.js";
|
|
24
24
|
import { getMockRows, hasMockFlag } from "./mock.js";
|
|
25
|
-
import { captureAppEvent } from "./analytics.js";
|
|
26
25
|
export { buildChoiceOutput } from "./agent_stream.js";
|
|
27
26
|
export function useWorkflow(alias) {
|
|
28
27
|
return useCallback((inputs) => rpc("workflow", { alias, inputs: inputs ?? {} }), [alias]);
|
|
@@ -288,7 +287,6 @@ export function useFileUpload() {
|
|
|
288
287
|
setError(null);
|
|
289
288
|
try {
|
|
290
289
|
const uploaded = await rpc("upload", { file, fidelity });
|
|
291
|
-
captureAppEvent("app_file_uploaded", { mime_type: file.type, fidelity });
|
|
292
290
|
return uploaded;
|
|
293
291
|
}
|
|
294
292
|
catch (err) {
|
|
@@ -601,14 +599,6 @@ export function useAgentRun(alias) {
|
|
|
601
599
|
error: "The run was interrupted and its result could not be confirmed. Run it again.",
|
|
602
600
|
};
|
|
603
601
|
safeSetState(acc);
|
|
604
|
-
// Fleet visibility: how often edges cut agent SSE streams. One
|
|
605
|
-
// event per truncation, with whether the row rescued the result.
|
|
606
|
-
captureAppEvent("app_agent_stream_truncated", {
|
|
607
|
-
kind: "clean_end",
|
|
608
|
-
recovered: settled != null,
|
|
609
|
-
settled_status: settled?.status ?? null,
|
|
610
|
-
has_output: acc.output !== undefined,
|
|
611
|
-
});
|
|
612
602
|
}
|
|
613
603
|
return landingOf(acc);
|
|
614
604
|
})
|
|
@@ -635,16 +625,6 @@ export function useAgentRun(alias) {
|
|
|
635
625
|
acc = adoptGuarded(acc, settled);
|
|
636
626
|
safeSetState(acc);
|
|
637
627
|
}
|
|
638
|
-
// The same fleet-visibility beacon as the clean-end path — a dropped
|
|
639
|
-
// connection is the OTHER way a stream dies mid-run, and it was
|
|
640
|
-
// previously invisible (the beacon fired only on clean-end
|
|
641
|
-
// truncations, undercounting every network-error cut).
|
|
642
|
-
captureAppEvent("app_agent_stream_truncated", {
|
|
643
|
-
kind: "connection_error",
|
|
644
|
-
recovered: settled != null,
|
|
645
|
-
settled_status: settled?.status ?? null,
|
|
646
|
-
has_output: acc.output !== undefined,
|
|
647
|
-
});
|
|
648
628
|
if (settled)
|
|
649
629
|
return landingOf(acc);
|
|
650
630
|
}
|
|
@@ -663,7 +643,6 @@ export function useAgentRun(alias) {
|
|
|
663
643
|
// in-flight promise makes a double-click resolve with the first run's
|
|
664
644
|
// result; `replace: true` is the explicit opt-in to abort-and-restart.
|
|
665
645
|
if (inflightRef.current && !opts.replace) {
|
|
666
|
-
captureAppEvent("app_agent_run_deduped", { alias });
|
|
667
646
|
return inflightRef.current;
|
|
668
647
|
}
|
|
669
648
|
handleRef.current?.abort();
|
|
@@ -710,6 +689,10 @@ export function useAgentRun(alias) {
|
|
|
710
689
|
const abort = useCallback(() => handleRef.current?.abort(), []);
|
|
711
690
|
const cancel = useCallback(() => {
|
|
712
691
|
// Stop server-side too (saves tokens on an unwanted run), then locally.
|
|
692
|
+
// No analytics event here: the cancel RPC stamps `app_agent_runs
|
|
693
|
+
// .cancel_requested_at`, which is what separates a user's Stop from the
|
|
694
|
+
// unmount/tab-close that `abort` also serves — a client event would only
|
|
695
|
+
// duplicate a column, and would go stale the moment an app pins an old SDK.
|
|
713
696
|
const runId = runIdRef.current;
|
|
714
697
|
if (runId)
|
|
715
698
|
void rpc("agentRun.cancel", { run_id: runId }).catch(() => { });
|
package/docs/ai.md
CHANGED
|
@@ -57,8 +57,8 @@ await recognize.run({ image_file_id: fileId }, { sessionId });
|
|
|
57
57
|
|
|
58
58
|
| Member | Type | What it is |
|
|
59
59
|
|---|---|---|
|
|
60
|
-
| `run` | `(input, { sessionId, replace? }) => Promise<AgentRunLanding<TOutput>>` | Start a run. Streams progress into the hook's state and resolves with **how the leg ended** — `{kind:"settled", output?, text}`, `{kind:"parked"}`, `{kind:"failed", error}` or `{kind:"aborted"}`. Switch on `kind`; do NOT read the hook's state to tell them apart, because it has not committed when the promise resolves (that race closed dialogs over live, answerable runs). A run failure is DATA here, so no try/catch is needed for it. **Single-flight:** while a run is in flight, calling it again returns the in-flight run's promise — an accidental double-press joins the first run instead of billing a second one
|
|
61
|
-
| `cancel` | `() => void` | Stop the run **server-side** (saves tokens) and locally. Wire a user-facing Stop button to this |
|
|
60
|
+
| `run` | `(input, { sessionId, replace? }) => Promise<AgentRunLanding<TOutput>>` | Start a run. Streams progress into the hook's state and resolves with **how the leg ended** — `{kind:"settled", output?, text}`, `{kind:"parked"}`, `{kind:"failed", error}` or `{kind:"aborted"}`. Switch on `kind`; do NOT read the hook's state to tell them apart, because it has not committed when the promise resolves (that race closed dialogs over live, answerable runs). A run failure is DATA here, so no try/catch is needed for it. **Single-flight:** while a run is in flight, calling it again returns the in-flight run's promise — an accidental double-press joins the first run instead of billing a second one. Pass `replace: true` to deliberately abort-and-restart; the replaced run still executes and bills server-side |
|
|
61
|
+
| `cancel` | `() => void` | Stop the run **server-side** (saves tokens) and locally. Wire a user-facing Stop button to this. The platform stamps `app_agent_runs.cancel_requested_at`, which is what distinguishes a user Stop from the unmount that `abort` also serves — so there is no client analytics event for it |
|
|
62
62
|
| `abort` | `() => void` | Stop listening **locally only** — the run keeps executing server-side and its result is still persisted. This is the unmount path (the hook calls it automatically on unmount) |
|
|
63
63
|
| `status` | `"idle" \| "streaming" \| "awaiting_input" \| "completed" \| "error"` | Whole-run state. `awaiting_input` = the run is PARKED on a question the agent asked (see the ask-back section below). `abort`/`cancel` reset it to `"idle"` (and clear the partial transcript) |
|
|
64
64
|
| `pendingChoice` | `PendingChoice \| null` | The agent's pending question(s) — non-null exactly while `status` is `awaiting_input`. `questions` maps 1:1 onto `@lotics/ui`'s `ClarifyWizard` (`{question, options: {label, description}[], allow_custom}`) |
|
|
@@ -184,7 +184,7 @@ The run's lifetime is decoupled from the stream: the server drives it to complet
|
|
|
184
184
|
- **The connection drops with an error** → the hook polls the persisted run to completion (every 2.5 s) and resolves with the settled result instead of surfacing a network error.
|
|
185
185
|
- **The stream ends cleanly WITHOUT a `finish` frame** — an edge/proxy can close a long SSE gracefully mid-run, which looks like completion but swallowed the trailing frames (including the structured result). The hook detects the missing `finish` and polls the row the same way.
|
|
186
186
|
|
|
187
|
-
|
|
187
|
+
Either way the run itself is unaffected — it keeps executing server-side and settles in `app_agent_runs`, which is where its outcome is read.
|
|
188
188
|
|
|
189
189
|
**Anonymous runs poll with a capability token, handled for you.** On a publicly-shared app a visitor has no member identity, so the server cannot authorize their poll by ownership — `triggered_by_member_id` is null by design. Instead the run response carries a per-run token (`x-app-agent-run-token`); the SDK stores it against that run id and replays it on the poll, on `cancel`, and on `answerChoice`. It is transport-level, like the password-session token — never surfaced to app code, and never issued for a member run (identity already authorizes those). Nothing to wire: an app calls `useAgentRun` the same way on both. The token is bound to ONE run, so it cannot read another visitor's, and it is why a dropped connection on a public app recovers rather than losing the answer.
|
|
190
190
|
|
package/docs/runtime.md
CHANGED
|
@@ -344,37 +344,29 @@ await checkIn({ latitude: r.coords.latitude, longitude: r.coords.longitude });
|
|
|
344
344
|
## Automatic analytics (PostHog)
|
|
345
345
|
|
|
346
346
|
`mount()` boots a PostHog instance per app — apps are a separate cross-origin
|
|
347
|
-
bundle, invisible to the product's own analytics.
|
|
348
|
-
install `posthog-js` or call any analytics API from app code
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
- Every event carries the app identity as super-properties (`app_id`,
|
|
365
|
-
`app_name`, `workspace_id`, `organization_id`) and rolls up under the
|
|
366
|
-
`organization` group. Embedded apps `identify` the signed-in member — app and
|
|
347
|
+
bundle, invisible to the product's own analytics. It is entirely automatic:
|
|
348
|
+
don't install `posthog-js` or call any analytics API from app code, and there is
|
|
349
|
+
no hook for app-defined events. If a bespoke funnel matters, request the surface
|
|
350
|
+
as a platform change.
|
|
351
|
+
|
|
352
|
+
The SDK emits one event, `app_opened`, once PostHog finishes initializing.
|
|
353
|
+
Everything an app *does* is already recorded by the platform — a comment, an
|
|
354
|
+
upload, a workflow or agent run each leave a row, and a run's outcome, including
|
|
355
|
+
a user's Stop (`cancel_requested_at`), lands in `app_agent_runs`.
|
|
356
|
+
|
|
357
|
+
- Autocapture, pageviews, dead clicks, and session replay are all off, set at
|
|
358
|
+
init rather than relied on project-wide: the server-side setting fails open,
|
|
359
|
+
and an app screen renders the customer's own records.
|
|
360
|
+
- `app_opened` carries the app identity as super-properties — `app_id`,
|
|
361
|
+
`workspace_id`, `organization_id`, ids only, since a name is the customer's
|
|
362
|
+
own words — and rolls up under the `organization` group. Embedded apps `identify` the signed-in member — app and
|
|
367
363
|
product events share one person; standalone visitors stay anonymous.
|
|
368
364
|
- Uncaught exceptions are captured (PostHog error tracking) in addition to
|
|
369
|
-
`mount()`'s visible banner.
|
|
365
|
+
`mount()`'s visible banner — the app's one error channel.
|
|
370
366
|
- **Tracking is gated to the deployed app host** (`*.lotics.app`). `lotics app
|
|
371
367
|
dev` (localhost) and any `?__mock=1` load emit nothing. Best-effort
|
|
372
|
-
throughout: a failed init or a failed `context` resolution
|
|
373
|
-
|
|
374
|
-
(bounded) and drained on init.
|
|
375
|
-
- **Limitation:** there is no public API for custom app events — the capture
|
|
376
|
-
function is internal to the SDK. If a bespoke funnel matters, request the
|
|
377
|
-
surface as a platform change.
|
|
368
|
+
throughout: a failed init or a failed `context` resolution leaves the app
|
|
369
|
+
fully working and untracked.
|
|
378
370
|
- **Limitation:** PostHog's default bot/user-agent filter applies — headless
|
|
379
371
|
browsers (e.g. Playwright) are never tracked, so analytics cannot be verified
|
|
380
372
|
through headless automation.
|