@civitai/blocks-react 0.67.1 → 0.69.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.
@@ -0,0 +1,191 @@
1
+ import type { BlockEstimateBatchAggregate, BlockWorkflowSnapshot, WorkflowBody } from '@civitai/app-sdk/blocks';
2
+ import { WorkflowEstimateError } from './useBuzzWorkflow.js';
3
+ export type { BlockEstimateBatchAggregate };
4
+ /**
5
+ * The most bodies one {@link UseBatchEstimate.estimateBatch} call may carry.
6
+ *
7
+ * The host's own cap (`BLOCK_ESTIMATE_BATCH_MAX_CELLS` in civitai/civitai). The
8
+ * hook refuses a longer list before sending it, with the code the host would
9
+ * have answered with a round trip later. A larger grid is priced in several
10
+ * calls; each call is still one request against the estimate allowance.
11
+ */
12
+ export declare const BATCH_ESTIMATE_MAX_CELLS = 16;
13
+ /**
14
+ * The host's `snapshot.error` for a batch cell its time budget ran out on before
15
+ * the cell priced. The copy is the host's (`BLOCK_ESTIMATE_BATCH_TIMEOUT_ERROR`
16
+ * in civitai/civitai `src/server/routers/blocks.router.ts`). This is the ONE
17
+ * place the SDK spells it: the hook maps it to {@link BatchEstimateCell}'s
18
+ * `timedOut`, and the mock host answers with it. Not exported from the package:
19
+ * apps branch on `timedOut`, never on this string.
20
+ */
21
+ export declare const BATCH_ESTIMATE_CELL_TIMEOUT_ERROR = "estimate timed out";
22
+ /**
23
+ * Why a WHOLE batch estimate was refused. The stable branch target on
24
+ * {@link BatchEstimateError}.
25
+ *
26
+ * - `'unsupported'` — the host answered `unsupported on this host`: it knows the
27
+ * message and has no handler for it on this surface. Nothing was priced. **Fall
28
+ * back to one `useBuzzWorkflow().estimate()` per cell.**
29
+ * - `'timeout'` — no reply arrived in time. A host that supports the message
30
+ * bounds its own work and replies well inside the default wait, marking any
31
+ * cell it could not price in time as an `ok: false` cell with `timedOut:
32
+ * true`, so this code usually means the host or its upstream is SLOW, not
33
+ * that it is old. 🔴 Do NOT fall back to per-cell estimates here:
34
+ * that sends up to 16 more estimates at an upstream that is already struggling.
35
+ * Show the estimate as unavailable and let the viewer retry later. A host that
36
+ * predates the message also ends here (it never replies); an app that must
37
+ * tell the two apart can probe once with a one-body batch and a short explicit
38
+ * `timeoutMs`, and treat only THAT probe's `'timeout'` as "no batch support".
39
+ * - `'invalid-request'` — the list is empty, is not an array, or holds more than
40
+ * {@link BATCH_ESTIMATE_MAX_CELLS} bodies. Refused by the hook before sending,
41
+ * or by the host. A per-cell fallback is pointless for an empty list and is the
42
+ * wrong fix for an over-long one: split it.
43
+ * - `'failed'` — the host or the server refused the call for another reason (the
44
+ * scope, the rate limit, review preview, no credential) or returned something
45
+ * that is not a batch result. The reason is on
46
+ * {@link BatchEstimateError.hostError}.
47
+ */
48
+ export type BatchEstimateErrorCode = 'unsupported' | 'timeout' | 'invalid-request' | 'failed';
49
+ /**
50
+ * Thrown by {@link UseBatchEstimate.estimateBatch} when the WHOLE call failed.
51
+ *
52
+ * One cell failing is NOT this: a cell that could not be priced comes back as an
53
+ * `ok: false` entry in {@link BatchEstimateResult.cells} and the call resolves.
54
+ *
55
+ * Same three-audience split as `WorkflowEstimateError`:
56
+ * - {@link BatchEstimateError.code} is what you branch on.
57
+ * - {@link BatchEstimateError.hostError} is the host's or server's own text. It
58
+ * is server-authored and unsanitised: log it, never render it.
59
+ * - `message` is a generic developer-facing summary whose wording is not a
60
+ * contract.
61
+ */
62
+ export declare class BatchEstimateError extends Error {
63
+ readonly code: BatchEstimateErrorCode;
64
+ /** The host's or server's reason, verbatim, when it gave one. */
65
+ readonly hostError?: string;
66
+ constructor(code: BatchEstimateErrorCode, hostError?: string);
67
+ }
68
+ /**
69
+ * One cell of a batch estimate — the answer to the body at the same index.
70
+ *
71
+ * - `ok: true` — the cell priced. `cost` is the cost object a single
72
+ * `estimate()` returns for that body, passed through untouched (`cost.total`
73
+ * is the price shown before `submit()`).
74
+ * - `ok: false` — the cell did not price. `error` is the SAME
75
+ * `WorkflowEstimateError` a single `estimate()` of that body would have
76
+ * rejected with (`code: 'failed' | 'no-cost'`, the reason on
77
+ * `error.snapshot.error`), so one function can turn either into viewer copy.
78
+ * It is a value here, not a throw: the other cells are still usable.
79
+ * - `timedOut` (on `ok: false` only) — `true` when the host's time budget for
80
+ * the call ran out before this cell priced (its `error.code` is `'failed'`).
81
+ * Show it as unavailable and retry later rather than re-estimating it at once.
82
+ * Branch on `timedOut`, never on `snapshot.error`: the host's wording is not a
83
+ * contract, and the hook maps it here so no app has to match it.
84
+ *
85
+ * `snapshot` is the host's snapshot for the cell, verbatim, on both arms.
86
+ */
87
+ export type BatchEstimateCell = {
88
+ ok: true;
89
+ cost: NonNullable<BlockWorkflowSnapshot['cost']>;
90
+ snapshot: BlockWorkflowSnapshot;
91
+ } | {
92
+ ok: false;
93
+ error: WorkflowEstimateError;
94
+ /** `true` when the host's time budget for the call ran out before this cell priced. */
95
+ timedOut: boolean;
96
+ snapshot: BlockWorkflowSnapshot;
97
+ };
98
+ /** What {@link UseBatchEstimate.estimateBatch} resolves with. */
99
+ export interface BatchEstimateResult {
100
+ /** One entry per body, in the order the bodies were given. */
101
+ cells: BatchEstimateCell[];
102
+ /**
103
+ * The host's run total over the cells that priced. It covers the whole list
104
+ * only when `aggregate.pricedCells === aggregate.cellCount`.
105
+ */
106
+ aggregate: BlockEstimateBatchAggregate;
107
+ }
108
+ /** Options for one {@link UseBatchEstimate.estimateBatch} call. */
109
+ export interface EstimateBatchOptions {
110
+ /**
111
+ * How long to wait for the host's reply, in milliseconds. Default
112
+ * `WORKFLOW_REQUEST_TIMEOUT_MS` (120,000), the single estimate's bound.
113
+ *
114
+ * A host that supports the batch replies within its own time budget, well
115
+ * inside this default, so leave it unset for ordinary pricing. Pass a short
116
+ * value only for a deliberate old-host probe (a one-body batch, sent once):
117
+ * a host that predates the message never replies, and a short wait finds that
118
+ * out sooner. A short wait on a full grid gives up on slow but healthy batches.
119
+ */
120
+ timeoutMs?: number;
121
+ }
122
+ /** What {@link useBatchEstimate} returns. */
123
+ export interface UseBatchEstimate {
124
+ /**
125
+ * Price several workflow bodies in one request. Resolves with one cell per
126
+ * body, in order, plus a run total; rejects with {@link BatchEstimateError}
127
+ * only when the whole call failed.
128
+ *
129
+ * 🔴 ESTIMATE ONLY. Nothing is submitted and no Buzz is held or spent. Submit
130
+ * each cell with `useBuzzWorkflow().submit()`, which asks the viewer and
131
+ * checks the budget per cell.
132
+ */
133
+ estimateBatch: (bodies: WorkflowBody[], options?: EstimateBatchOptions) => Promise<BatchEstimateResult>;
134
+ /** `true` while the most recent call is in flight. */
135
+ pending: boolean;
136
+ /**
137
+ * The most recent call's result, or `null` before the first one and after a
138
+ * call that failed — a failed call never leaves an earlier total on display.
139
+ */
140
+ result: BatchEstimateResult | null;
141
+ /** The most recent call's error, or `null`. */
142
+ error: BatchEstimateError | null;
143
+ }
144
+ /**
145
+ * Price a grid of generations in ONE request: a list of workflow bodies in, one
146
+ * result per body (a cost, or that cell's error) plus a run total out. Backed by
147
+ * the `ESTIMATE_WORKFLOW_BATCH` → `ESTIMATE_BATCH_RESULT` bridge.
148
+ *
149
+ * Use it where an app would otherwise call `useBuzzWorkflow().estimate()` once
150
+ * per cell. Every rule a single estimate enforces still applies to each cell;
151
+ * what changes is the accounting and the round trips:
152
+ *
153
+ * - **One request against the estimate allowance**, whatever the number of
154
+ * cells. On a page app that allowance is shared by every viewer of the app and
155
+ * by the host's other reads for it, so a 16-cell grid used to take 16 of it per
156
+ * pricing pass.
157
+ * - **Cells are metered separately.** The host also counts the cells of every
158
+ * batch against an allowance of their own, shared like the estimate allowance
159
+ * (on a page app, by every viewer of the app), so a batch is not a way around
160
+ * it. A call over either is refused whole (`code: 'failed'`, the host's
161
+ * rate-limit text on `hostError`); retry in a few seconds.
162
+ * - **At most {@link BATCH_ESTIMATE_MAX_CELLS} bodies per call.**
163
+ * - **`kind: 'training'` bodies are refused per cell.** Estimate a training run
164
+ * with `useBuzzWorkflow().estimate()`.
165
+ *
166
+ * 🔴 ESTIMATE ONLY. There is no batch submit and no server-side id for the
167
+ * list. To watch and cancel the submitted cells together, use
168
+ * `useWorkflowBatch()`. The total is a quote: nothing reserves it and each cell
169
+ * is priced again at `submit()`.
170
+ *
171
+ * Like `estimate()`, this does NOT prompt for consent. It needs the
172
+ * `ai:write:budgeted` scope; without it the call rejects with `code: 'failed'`.
173
+ *
174
+ * @example
175
+ * const { estimateBatch } = useBatchEstimate();
176
+ * try {
177
+ * const { cells, aggregate } = await estimateBatch(bodies);
178
+ * // cells[i] answers bodies[i]; a cell with `!c.ok && c.timedOut` was not
179
+ * // priced in time — show it as unavailable and retry later
180
+ * } catch (err) {
181
+ * if (!(err instanceof BatchEstimateError)) throw err;
182
+ * if (err.code === 'unsupported') {
183
+ * // this host has no batch estimate — price each cell with estimate()
184
+ * } else {
185
+ * // 'timeout' (a slow host, not an old one) or 'failed': show the estimate
186
+ * // as unavailable and retry later — never one estimate() per cell
187
+ * }
188
+ * }
189
+ */
190
+ export declare function useBatchEstimate(): UseBatchEstimate;
191
+ //# sourceMappingURL=useBatchEstimate.d.ts.map
@@ -0,0 +1,196 @@
1
+ import { useCallback, useState } from 'react';
2
+ import { WORKFLOW_REQUEST_TIMEOUT_MS } from '../transport/requestTimeouts.js';
3
+ import { getTransport } from '../transport/singleton.js';
4
+ import { RequestTimeoutError, sendTypedRequest } from '../transport/transport.js';
5
+ import { WorkflowEstimateError } from './useBuzzWorkflow.js';
6
+ import { useRequestSequencer } from './useRequestSequencer.js';
7
+ /**
8
+ * The most bodies one {@link UseBatchEstimate.estimateBatch} call may carry.
9
+ *
10
+ * The host's own cap (`BLOCK_ESTIMATE_BATCH_MAX_CELLS` in civitai/civitai). The
11
+ * hook refuses a longer list before sending it, with the code the host would
12
+ * have answered with a round trip later. A larger grid is priced in several
13
+ * calls; each call is still one request against the estimate allowance.
14
+ */
15
+ export const BATCH_ESTIMATE_MAX_CELLS = 16;
16
+ /**
17
+ * The host's generic reply to a request it registers no handler for. The copy is
18
+ * the host's (`BRIDGE_NACK_NO_HANDLER` in civitai/civitai), matched here ONCE so
19
+ * no app has to match it: branch on {@link BatchEstimateError.code} instead.
20
+ */
21
+ const HOST_UNSUPPORTED_REPLY = 'unsupported on this host';
22
+ /**
23
+ * The host's `snapshot.error` for a batch cell its time budget ran out on before
24
+ * the cell priced. The copy is the host's (`BLOCK_ESTIMATE_BATCH_TIMEOUT_ERROR`
25
+ * in civitai/civitai `src/server/routers/blocks.router.ts`). This is the ONE
26
+ * place the SDK spells it: the hook maps it to {@link BatchEstimateCell}'s
27
+ * `timedOut`, and the mock host answers with it. Not exported from the package:
28
+ * apps branch on `timedOut`, never on this string.
29
+ */
30
+ export const BATCH_ESTIMATE_CELL_TIMEOUT_ERROR = 'estimate timed out';
31
+ /**
32
+ * Thrown by {@link UseBatchEstimate.estimateBatch} when the WHOLE call failed.
33
+ *
34
+ * One cell failing is NOT this: a cell that could not be priced comes back as an
35
+ * `ok: false` entry in {@link BatchEstimateResult.cells} and the call resolves.
36
+ *
37
+ * Same three-audience split as `WorkflowEstimateError`:
38
+ * - {@link BatchEstimateError.code} is what you branch on.
39
+ * - {@link BatchEstimateError.hostError} is the host's or server's own text. It
40
+ * is server-authored and unsanitised: log it, never render it.
41
+ * - `message` is a generic developer-facing summary whose wording is not a
42
+ * contract.
43
+ */
44
+ export class BatchEstimateError extends Error {
45
+ code;
46
+ /** The host's or server's reason, verbatim, when it gave one. */
47
+ hostError;
48
+ constructor(code, hostError) {
49
+ super(`batch estimate failed (${code})${hostError === undefined ? '' : ' — reason on .hostError'}`);
50
+ this.name = 'BatchEstimateError';
51
+ this.code = code;
52
+ if (hostError !== undefined)
53
+ this.hostError = hostError;
54
+ }
55
+ }
56
+ function toCell(snapshot) {
57
+ // The rule `useBuzzWorkflow().estimate()` applies to one reply, per cell.
58
+ if (snapshot.status === 'failed') {
59
+ return {
60
+ ok: false,
61
+ error: new WorkflowEstimateError(snapshot, 'failed'),
62
+ // The one place the host's per-cell timeout string is matched.
63
+ timedOut: snapshot.error === BATCH_ESTIMATE_CELL_TIMEOUT_ERROR,
64
+ snapshot,
65
+ };
66
+ }
67
+ const cost = snapshot.cost;
68
+ if (!cost || typeof cost.total !== 'number') {
69
+ return {
70
+ ok: false,
71
+ error: new WorkflowEstimateError(snapshot, 'no-cost'),
72
+ timedOut: false,
73
+ snapshot,
74
+ };
75
+ }
76
+ return { ok: true, cost, snapshot };
77
+ }
78
+ function toBatchEstimateError(err) {
79
+ if (err instanceof BatchEstimateError)
80
+ return err;
81
+ // Structural, not a message match — see `RequestTimeoutError`.
82
+ if (err instanceof RequestTimeoutError)
83
+ return new BatchEstimateError('timeout', err.message);
84
+ return new BatchEstimateError('failed', err instanceof Error ? err.message : String(err));
85
+ }
86
+ /**
87
+ * Price a grid of generations in ONE request: a list of workflow bodies in, one
88
+ * result per body (a cost, or that cell's error) plus a run total out. Backed by
89
+ * the `ESTIMATE_WORKFLOW_BATCH` → `ESTIMATE_BATCH_RESULT` bridge.
90
+ *
91
+ * Use it where an app would otherwise call `useBuzzWorkflow().estimate()` once
92
+ * per cell. Every rule a single estimate enforces still applies to each cell;
93
+ * what changes is the accounting and the round trips:
94
+ *
95
+ * - **One request against the estimate allowance**, whatever the number of
96
+ * cells. On a page app that allowance is shared by every viewer of the app and
97
+ * by the host's other reads for it, so a 16-cell grid used to take 16 of it per
98
+ * pricing pass.
99
+ * - **Cells are metered separately.** The host also counts the cells of every
100
+ * batch against an allowance of their own, shared like the estimate allowance
101
+ * (on a page app, by every viewer of the app), so a batch is not a way around
102
+ * it. A call over either is refused whole (`code: 'failed'`, the host's
103
+ * rate-limit text on `hostError`); retry in a few seconds.
104
+ * - **At most {@link BATCH_ESTIMATE_MAX_CELLS} bodies per call.**
105
+ * - **`kind: 'training'` bodies are refused per cell.** Estimate a training run
106
+ * with `useBuzzWorkflow().estimate()`.
107
+ *
108
+ * 🔴 ESTIMATE ONLY. There is no batch submit and no server-side id for the
109
+ * list. To watch and cancel the submitted cells together, use
110
+ * `useWorkflowBatch()`. The total is a quote: nothing reserves it and each cell
111
+ * is priced again at `submit()`.
112
+ *
113
+ * Like `estimate()`, this does NOT prompt for consent. It needs the
114
+ * `ai:write:budgeted` scope; without it the call rejects with `code: 'failed'`.
115
+ *
116
+ * @example
117
+ * const { estimateBatch } = useBatchEstimate();
118
+ * try {
119
+ * const { cells, aggregate } = await estimateBatch(bodies);
120
+ * // cells[i] answers bodies[i]; a cell with `!c.ok && c.timedOut` was not
121
+ * // priced in time — show it as unavailable and retry later
122
+ * } catch (err) {
123
+ * if (!(err instanceof BatchEstimateError)) throw err;
124
+ * if (err.code === 'unsupported') {
125
+ * // this host has no batch estimate — price each cell with estimate()
126
+ * } else {
127
+ * // 'timeout' (a slow host, not an old one) or 'failed': show the estimate
128
+ * // as unavailable and retry later — never one estimate() per cell
129
+ * }
130
+ * }
131
+ */
132
+ export function useBatchEstimate() {
133
+ const [pending, setPending] = useState(false);
134
+ const [result, setResult] = useState(null);
135
+ const [error, setError] = useState(null);
136
+ // Latest call wins: an app re-prices on a form edit, so an older reply landing
137
+ // after a newer one must not put the older total back on display.
138
+ const sequencer = useRequestSequencer();
139
+ const estimateBatch = useCallback(async (bodies, options) => {
140
+ const call = sequencer.begin();
141
+ if (sequencer.isCurrent(call)) {
142
+ setPending(true);
143
+ setError(null);
144
+ }
145
+ try {
146
+ if (!Array.isArray(bodies) || bodies.length === 0) {
147
+ throw new BatchEstimateError('invalid-request', 'invalid estimate batch');
148
+ }
149
+ if (bodies.length > BATCH_ESTIMATE_MAX_CELLS) {
150
+ throw new BatchEstimateError('invalid-request', 'estimate batch too large');
151
+ }
152
+ const reply = await sendTypedRequest(getTransport(), { type: 'ESTIMATE_WORKFLOW_BATCH', payload: { bodies } }, 'ESTIMATE_BATCH_RESULT',
153
+ // Server-bound like the single estimate, and slower: up to 16 cost
154
+ // quotes behind one reply.
155
+ { timeoutMs: options?.timeoutMs ?? WORKFLOW_REQUEST_TIMEOUT_MS });
156
+ if (reply.error !== undefined) {
157
+ if (reply.error === HOST_UNSUPPORTED_REPLY) {
158
+ throw new BatchEstimateError('unsupported', reply.error);
159
+ }
160
+ if (reply.error === 'invalid estimate batch' ||
161
+ reply.error === 'estimate batch too large') {
162
+ throw new BatchEstimateError('invalid-request', reply.error);
163
+ }
164
+ throw new BatchEstimateError('failed', reply.error);
165
+ }
166
+ const { snapshots, aggregate } = reply;
167
+ if (!snapshots || !aggregate || snapshots.length !== bodies.length) {
168
+ // A reply that does not answer every body cannot be lined up with the
169
+ // grid, so it is refused rather than shown shifted by a cell.
170
+ throw new BatchEstimateError('failed', 'the host returned a malformed batch estimate');
171
+ }
172
+ const next = { cells: snapshots.map(toCell), aggregate };
173
+ if (sequencer.isCurrent(call))
174
+ setResult(next);
175
+ return next;
176
+ }
177
+ catch (err) {
178
+ const wrapped = toBatchEstimateError(err);
179
+ if (sequencer.isCurrent(call)) {
180
+ setError(wrapped);
181
+ // Fail closed: a confirm gate reading `result` must not keep quoting the
182
+ // previous list's total after this one failed.
183
+ setResult(null);
184
+ }
185
+ // Thrown even when superseded or unmounted — the caller's `await` is not
186
+ // the component.
187
+ throw wrapped;
188
+ }
189
+ finally {
190
+ if (sequencer.isCurrent(call))
191
+ setPending(false);
192
+ }
193
+ }, [sequencer]);
194
+ return { estimateBatch, pending, result, error };
195
+ }
196
+ //# sourceMappingURL=useBatchEstimate.js.map
@@ -1,15 +1,57 @@
1
1
  /** What {@link useBlockAnalytics} returns. */
2
2
  export interface UseBlockAnalytics {
3
+ /**
4
+ * Send one custom event. Fire-and-forget: returns nothing, and nothing tells
5
+ * you whether it was recorded. See {@link useBlockAnalytics}
6
+ * for what is kept.
7
+ */
3
8
  track: (eventName: string, properties?: Record<string, unknown>) => void;
4
9
  }
5
10
  /**
6
- * Fire-and-forget analytics tracking. The host forwards events to its
7
- * analytics pipeline (ClickHouse in production); the block doesn't see
8
- * acknowledgements and shouldn't block on them.
11
+ * Custom events for your app's analytics panel. Fire-and-forget: the host
12
+ * forwards the event and the block gets no acknowledgement, so never wait on
13
+ * one or branch on it.
14
+ *
15
+ * 🔴 **Only what your manifest DECLARES is recorded.** Declare every event, and
16
+ * every property it carries, under `analytics.events` in `block.manifest.json`.
17
+ * The declaration is reviewed with the rest of the manifest, and events are
18
+ * accepted against the APPROVED version:
19
+ *
20
+ * - **An undeclared event** is counted as dropped. Its name and properties are
21
+ * not stored; the owner sees only a count of drops.
22
+ * - **A declared event keeps only its declared properties**, each only when the
23
+ * value has the declared type. An undeclared property, or a value of the
24
+ * wrong type, is dropped and the rest of the event is kept.
25
+ * - **Property types are `enum`, `number` and `boolean`.** There is no free-text
26
+ * string: a string is kept only when it is one of the enum's declared values.
27
+ * So a prompt, an email or a username cannot be recorded. A `number` is not
28
+ * range-limited, so do not use one to carry an identifier.
29
+ * - Names are lowercase snake_case: a letter, then up to 63 of `a-z`, `0-9`, `_`.
30
+ * At most 50 events per app and 10 properties per event.
31
+ *
32
+ * **Limits.** Each mount of your app may send at most 10 events per second and
33
+ * 500 in total; the host batches events into bodies of at most 32 KiB. An event
34
+ * over a limit is dropped in the browser and is not counted anywhere, so the
35
+ * panel's numbers are floors. Events are recorded only from the production
36
+ * site.
37
+ *
38
+ * **What owners see.** Aggregates in the app's analytics panel: per declared
39
+ * event, the count, unique signed-in viewers, and the declared properties'
40
+ * breakdowns. Signed-out viewers are counted as events but never identified,
41
+ * and the team's own activity is excluded by default. Individual events are
42
+ * never shown.
43
+ *
44
+ * **Check locally.** In `dev:live`, pass your manifest to `createLiveHost`
45
+ * (`createLiveHost({ blockToken, manifest })`) and the console warns once per
46
+ * undeclared event, undeclared property, or value of the wrong type.
9
47
  *
10
48
  * @example
49
+ * // block.manifest.json
50
+ * // "analytics": { "events": { "generate_clicked": { "properties": {
51
+ * // "mode": { "type": "enum", "values": ["txt2img", "img2img"] },
52
+ * // "batch": { "type": "number" } } } } }
11
53
  * const { track } = useBlockAnalytics();
12
- * track('generate_clicked', { modelId });
54
+ * track('generate_clicked', { mode: 'txt2img', batch: 4 });
13
55
  */
14
56
  export declare function useBlockAnalytics(): UseBlockAnalytics;
15
57
  //# sourceMappingURL=useBlockAnalytics.d.ts.map
@@ -1,13 +1,50 @@
1
1
  import { useCallback } from 'react';
2
2
  import { getTransport } from '../transport/singleton.js';
3
3
  /**
4
- * Fire-and-forget analytics tracking. The host forwards events to its
5
- * analytics pipeline (ClickHouse in production); the block doesn't see
6
- * acknowledgements and shouldn't block on them.
4
+ * Custom events for your app's analytics panel. Fire-and-forget: the host
5
+ * forwards the event and the block gets no acknowledgement, so never wait on
6
+ * one or branch on it.
7
+ *
8
+ * 🔴 **Only what your manifest DECLARES is recorded.** Declare every event, and
9
+ * every property it carries, under `analytics.events` in `block.manifest.json`.
10
+ * The declaration is reviewed with the rest of the manifest, and events are
11
+ * accepted against the APPROVED version:
12
+ *
13
+ * - **An undeclared event** is counted as dropped. Its name and properties are
14
+ * not stored; the owner sees only a count of drops.
15
+ * - **A declared event keeps only its declared properties**, each only when the
16
+ * value has the declared type. An undeclared property, or a value of the
17
+ * wrong type, is dropped and the rest of the event is kept.
18
+ * - **Property types are `enum`, `number` and `boolean`.** There is no free-text
19
+ * string: a string is kept only when it is one of the enum's declared values.
20
+ * So a prompt, an email or a username cannot be recorded. A `number` is not
21
+ * range-limited, so do not use one to carry an identifier.
22
+ * - Names are lowercase snake_case: a letter, then up to 63 of `a-z`, `0-9`, `_`.
23
+ * At most 50 events per app and 10 properties per event.
24
+ *
25
+ * **Limits.** Each mount of your app may send at most 10 events per second and
26
+ * 500 in total; the host batches events into bodies of at most 32 KiB. An event
27
+ * over a limit is dropped in the browser and is not counted anywhere, so the
28
+ * panel's numbers are floors. Events are recorded only from the production
29
+ * site.
30
+ *
31
+ * **What owners see.** Aggregates in the app's analytics panel: per declared
32
+ * event, the count, unique signed-in viewers, and the declared properties'
33
+ * breakdowns. Signed-out viewers are counted as events but never identified,
34
+ * and the team's own activity is excluded by default. Individual events are
35
+ * never shown.
36
+ *
37
+ * **Check locally.** In `dev:live`, pass your manifest to `createLiveHost`
38
+ * (`createLiveHost({ blockToken, manifest })`) and the console warns once per
39
+ * undeclared event, undeclared property, or value of the wrong type.
7
40
  *
8
41
  * @example
42
+ * // block.manifest.json
43
+ * // "analytics": { "events": { "generate_clicked": { "properties": {
44
+ * // "mode": { "type": "enum", "values": ["txt2img", "img2img"] },
45
+ * // "batch": { "type": "number" } } } } }
9
46
  * const { track } = useBlockAnalytics();
10
- * track('generate_clicked', { modelId });
47
+ * track('generate_clicked', { mode: 'txt2img', batch: 4 });
11
48
  */
12
49
  export function useBlockAnalytics() {
13
50
  const track = useCallback((eventName, properties) => {
@@ -207,7 +207,10 @@ export declare class WorkflowEstimateError extends Error {
207
207
  * rather than on prose. It is the ONLY stable branch target here:
208
208
  * `snapshot.error` is server-authored and can change without notice, and
209
209
  * {@link WorkflowEstimateError.message} is a generic developer-facing summary
210
- * whose exact wording is not a contract either.
210
+ * whose exact wording is not a contract either. The one carve-out is a batch
211
+ * cell the host's time budget ran out on: it keeps `code: 'failed'`, and its
212
+ * stable signal is `timedOut: true` on the `useBatchEstimate()` cell, never a
213
+ * match on `snapshot.error`.
211
214
  *
212
215
  * 🔴 THIS IS WHAT A VIEWER-FACING MESSAGE MUST BE DERIVED FROM. Branch on
213
216
  * `code` and return a string your app owns and localises; never render
@@ -191,7 +191,10 @@ export class WorkflowEstimateError extends Error {
191
191
  * rather than on prose. It is the ONLY stable branch target here:
192
192
  * `snapshot.error` is server-authored and can change without notice, and
193
193
  * {@link WorkflowEstimateError.message} is a generic developer-facing summary
194
- * whose exact wording is not a contract either.
194
+ * whose exact wording is not a contract either. The one carve-out is a batch
195
+ * cell the host's time budget ran out on: it keeps `code: 'failed'`, and its
196
+ * stable signal is `timedOut: true` on the `useBatchEstimate()` cell, never a
197
+ * match on `snapshot.error`.
195
198
  *
196
199
  * 🔴 THIS IS WHAT A VIEWER-FACING MESSAGE MUST BE DERIVED FROM. Branch on
197
200
  * `code` and return a string your app owns and localises; never render