@fixback/expo 0.8.3 → 0.9.1
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/README.md +5 -7
- package/dist/FeedbackModal.d.ts +7 -2
- package/dist/FeedbackModal.js +30 -3
- package/dist/FeedbackModal.js.map +1 -1
- package/dist/FixbackProvider.js +5 -1
- package/dist/FixbackProvider.js.map +1 -1
- package/dist/adapters.js +5 -4
- package/dist/adapters.js.map +1 -1
- package/dist/breadcrumbs.d.ts +15 -51
- package/dist/breadcrumbs.js +22 -169
- package/dist/breadcrumbs.js.map +1 -1
- package/dist/client-boot.d.ts +40 -0
- package/dist/client-boot.js +269 -0
- package/dist/client-boot.js.map +1 -0
- package/dist/client-connect.d.ts +33 -0
- package/dist/client-connect.js +306 -0
- package/dist/client-connect.js.map +1 -0
- package/dist/client-replay.d.ts +31 -0
- package/dist/client-replay.js +165 -0
- package/dist/client-replay.js.map +1 -0
- package/dist/client-state.d.ts +131 -0
- package/dist/client-state.js +158 -0
- package/dist/client-state.js.map +1 -0
- package/dist/client.d.ts +10 -245
- package/dist/client.js +80 -787
- package/dist/client.js.map +1 -1
- package/dist/error-capture.js +6 -26
- package/dist/error-capture.js.map +1 -1
- package/dist/identity.d.ts +0 -2
- package/dist/identity.js +0 -3
- package/dist/identity.js.map +1 -1
- package/dist/options.d.ts +263 -0
- package/dist/options.js +12 -0
- package/dist/options.js.map +1 -0
- package/dist/report.d.ts +13 -12
- package/dist/report.js +19 -38
- package/dist/report.js.map +1 -1
- package/dist/submit.d.ts +10 -37
- package/dist/submit.js +14 -64
- package/dist/submit.js.map +1 -1
- package/dist/tokens.d.ts +8 -3
- package/dist/tokens.js +12 -6
- package/dist/tokens.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -3
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* **Connect** on the headless client (ADR-0066): the two doors `signIn()` opens
|
|
4
|
+
* (ADR-0052), the inbound deep links that finish a Connect started outside the app
|
|
5
|
+
* (ADR-0046), and `signOut()`.
|
|
6
|
+
*
|
|
7
|
+
* One piece of the headless client (`./client`), created over the state every piece
|
|
8
|
+
* shares (`./client-state`): the Reporter session token, the connect URL boot
|
|
9
|
+
* returned, the last inbound code, and the callers waiting on one. A Connect that lands
|
|
10
|
+
* re-boots as the Account through the boot it is handed (`./client-boot`), so a gated
|
|
11
|
+
* Project's shake gesture arms once the Account is eligible.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.canSignIn = canSignIn;
|
|
15
|
+
exports.createConnect = createConnect;
|
|
16
|
+
const sdk_core_1 = require("@fixback/sdk-core");
|
|
17
|
+
const client_state_1 = require("./client-state");
|
|
18
|
+
const connect_1 = require("./connect");
|
|
19
|
+
const reporter_session_store_1 = require("./reporter-session-store");
|
|
20
|
+
/**
|
|
21
|
+
* How long the browser door's `signIn()` waits for the connect page's redirect to come
|
|
22
|
+
* back as a deep link (ADR-0052). Generous, because the round trip contains a real
|
|
23
|
+
* sign-in — the doors, an email link, a password manager — and the only cost of
|
|
24
|
+
* waiting is one pending promise. It exists so a person who abandons the page settles
|
|
25
|
+
* that promise eventually rather than never; the Connect itself is not cancelled by it
|
|
26
|
+
* and still lands through the inbound listener if they come back later.
|
|
27
|
+
*/
|
|
28
|
+
const CONNECT_BROWSER_WAIT_MS = 5 * 60_000;
|
|
29
|
+
/**
|
|
30
|
+
* Whether the browser door is available: something to open the page with, and the
|
|
31
|
+
* `linking` seam the code comes back on. Without the seam the page would land the
|
|
32
|
+
* person back in the app with a code nothing reads, which is worse than not opening
|
|
33
|
+
* it — so both are required, not either.
|
|
34
|
+
*/
|
|
35
|
+
function canConnectThroughBrowser({ adapters }) {
|
|
36
|
+
return Boolean(adapters.openBrowser && adapters.linking);
|
|
37
|
+
}
|
|
38
|
+
/** Whether `signIn()` can open a door at all — what the dormancy hint is gated on. */
|
|
39
|
+
function canSignIn(s) {
|
|
40
|
+
const { options, adapters } = s;
|
|
41
|
+
return Boolean((options.returnUrl && adapters.openAuthSession) || canConnectThroughBrowser(s));
|
|
42
|
+
}
|
|
43
|
+
/** Why `signIn()` could not open a door, named by what this app is actually missing. */
|
|
44
|
+
function explainSignInDoor({ adapters }) {
|
|
45
|
+
if (!adapters.openBrowser && !adapters.openAuthSession) {
|
|
46
|
+
return `${sdk_core_1.WARN_PREFIX} signIn needs a browser to open the connect page — install expo-web-browser.`;
|
|
47
|
+
}
|
|
48
|
+
if (!adapters.linking) {
|
|
49
|
+
return `${sdk_core_1.WARN_PREFIX} ` + "signIn needs the Linking seam to receive the connect page's redirect — or declare a `returnUrl` in init to return through an auth session instead.";
|
|
50
|
+
}
|
|
51
|
+
return `${sdk_core_1.WARN_PREFIX} ` + "signIn needs either an auth-session opener plus a `returnUrl` in init, or an in-app browser (install expo-web-browser).";
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Connect over the client's shared state. `bootAndArm` is the client's boot: a Connect
|
|
55
|
+
* that lands re-boots through it, as the Account.
|
|
56
|
+
*/
|
|
57
|
+
function createConnect(s, bootAndArm) {
|
|
58
|
+
const { options, adapters, warn, key, apiUrl } = s;
|
|
59
|
+
/**
|
|
60
|
+
* Start a **Connect** (ADR-0066): open the platform's connect page, exchange the
|
|
61
|
+
* one-time code it hands back for a Reporter session, store it in SecureStore, and
|
|
62
|
+
* re-boot as the Account — so a gated Project's shake gesture arms once the Account
|
|
63
|
+
* is eligible. Exposed so a host app can put it on its own settings screen (the way
|
|
64
|
+
* a gated Project, which shows nothing to a signed-out visitor, is entered), and
|
|
65
|
+
* behind the composer's identity row. Never throws: any failure leaves the current
|
|
66
|
+
* identity untouched.
|
|
67
|
+
*
|
|
68
|
+
* There are two doors, and which one opens depends only on what this app can offer
|
|
69
|
+
* (ADR-0052):
|
|
70
|
+
*
|
|
71
|
+
* - **The auth session**, when `init` declared a `returnUrl` and an opener is
|
|
72
|
+
* installed. The page returns *in place*, so the person never leaves the app.
|
|
73
|
+
* - **The in-app browser**, otherwise. The page is opened with no return URL and
|
|
74
|
+
* resolves where to land from the app links the *Project* lists, then redirects
|
|
75
|
+
* to one — which re-opens this app, where the inbound listener redeems the code
|
|
76
|
+
* (the journey a claimed Invite already travels). This is what keeps `signIn()`
|
|
77
|
+
* from being inert on the default install, which declares no `returnUrl` at all.
|
|
78
|
+
*/
|
|
79
|
+
async function signIn() {
|
|
80
|
+
const gen = s.generation;
|
|
81
|
+
if (!s.connectUrl)
|
|
82
|
+
return (0, client_state_1.reporterIdentity)(s); // not booted, or an older server
|
|
83
|
+
try {
|
|
84
|
+
if (options.returnUrl && adapters.openAuthSession) {
|
|
85
|
+
const outcome = await (0, connect_1.startExpoConnect)(adapters.openAuthSession, {
|
|
86
|
+
connectUrl: s.connectUrl,
|
|
87
|
+
key,
|
|
88
|
+
returnUrl: options.returnUrl,
|
|
89
|
+
});
|
|
90
|
+
if (gen !== s.generation)
|
|
91
|
+
return (0, client_state_1.reporterIdentity)(s);
|
|
92
|
+
if (outcome.kind === "code")
|
|
93
|
+
await redeemConnectCode(outcome.code, gen);
|
|
94
|
+
}
|
|
95
|
+
else if (canConnectThroughBrowser(s)) {
|
|
96
|
+
await connectThroughBrowser(gen);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
warn(explainSignInDoor(s));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// A Connect problem must never disturb the host app.
|
|
104
|
+
}
|
|
105
|
+
return (0, client_state_1.reporterIdentity)(s);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The browser door (ADR-0052): open the connect page with no return URL and wait for
|
|
109
|
+
* the app link it redirects to, which the OS delivers back here as a deep link.
|
|
110
|
+
*
|
|
111
|
+
* The wait matters because the two halves are separated by the operating system: the
|
|
112
|
+
* page redirects, the app is foregrounded, and {@link handleInboundUrl} redeems —
|
|
113
|
+
* all after `openBrowser` has resolved (on Android it resolves as soon as the tab is
|
|
114
|
+
* shown). Without it, an awaited `signIn()` would report the anonymous identity it
|
|
115
|
+
* started from while the Connect it started was still completing. It is bounded, so
|
|
116
|
+
* a person who wandered off never leaves a promise pending for the life of the app.
|
|
117
|
+
*/
|
|
118
|
+
async function connectThroughBrowser(gen) {
|
|
119
|
+
const landed = awaitInboundConnect();
|
|
120
|
+
try {
|
|
121
|
+
await adapters.openBrowser((0, connect_1.buildAppConnectUrl)(s.connectUrl, key));
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
settleInboundWaiters(false);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (gen !== s.generation)
|
|
128
|
+
return settleInboundWaiters(false);
|
|
129
|
+
// The redirect re-opens this app; dismiss the page it came from so the person is
|
|
130
|
+
// returned to where they tapped Sign in rather than to a finished connect page.
|
|
131
|
+
if (await landed)
|
|
132
|
+
dismissBrowser();
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* A promise for the next inbound Connect — `true` when one is redeemed, `false` when
|
|
136
|
+
* the wait expires or the client is destroyed. The timer is cleared on either, so a
|
|
137
|
+
* completed sign-in leaves nothing running.
|
|
138
|
+
*/
|
|
139
|
+
function awaitInboundConnect() {
|
|
140
|
+
return new Promise((resolve) => {
|
|
141
|
+
const timer = setTimeout(() => settleInboundWaiters(false), CONNECT_BROWSER_WAIT_MS);
|
|
142
|
+
s.inboundWaiters.push((landed) => {
|
|
143
|
+
clearTimeout(timer);
|
|
144
|
+
resolve(landed);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/** Resolve everyone waiting on an inbound Connect, and stop waiting. */
|
|
149
|
+
function settleInboundWaiters(landed) {
|
|
150
|
+
const waiting = s.inboundWaiters;
|
|
151
|
+
s.inboundWaiters = [];
|
|
152
|
+
for (const resolve of waiting) {
|
|
153
|
+
try {
|
|
154
|
+
resolve(landed);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
/* a waiter must never break the client */
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** Close the in-app browser this app opened — best-effort, like every seam here. */
|
|
162
|
+
function dismissBrowser() {
|
|
163
|
+
try {
|
|
164
|
+
adapters.dismissBrowser?.();
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
/* dismissing is a courtesy, never a failure */
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Exchange a one-time Connect code for a Reporter session and persist it, leaving the
|
|
172
|
+
* client identified as the Account. The tail every half of Connect on mobile shares:
|
|
173
|
+
* {@link signIn}, where the app opened the connect page itself, and
|
|
174
|
+
* {@link handleInboundUrl}, where the code arrived on a deep link from an invite the
|
|
175
|
+
* person claimed elsewhere (ADR-0046). `gen` is the caller's lifecycle generation,
|
|
176
|
+
* re-checked after every await so a destroyed client abandons the redemption.
|
|
177
|
+
*
|
|
178
|
+
* Booting on the new identity is the **caller's** step, because the two callers
|
|
179
|
+
* differ: a redemption mid-run must re-boot to arm, while the one on the launch URL
|
|
180
|
+
* is followed by `start`'s own boot and would only duplicate it.
|
|
181
|
+
*/
|
|
182
|
+
async function exchangeAndStore(code, gen) {
|
|
183
|
+
const result = await (0, sdk_core_1.exchangeConnectCode)(apiUrl, {
|
|
184
|
+
key,
|
|
185
|
+
code,
|
|
186
|
+
anonymousId: s.identity.anonymousId,
|
|
187
|
+
platform: "expo",
|
|
188
|
+
}, { fetch: adapters.fetch });
|
|
189
|
+
if (gen !== s.generation || !result)
|
|
190
|
+
return false;
|
|
191
|
+
s.reporterSessionToken = result.reporterSession;
|
|
192
|
+
s.identity = { ...s.identity, reporterSession: s.reporterSessionToken };
|
|
193
|
+
await (0, reporter_session_store_1.writeReporterSession)(adapters.secureStore, key, s.reporterSessionToken);
|
|
194
|
+
return gen === s.generation;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* {@link exchangeAndStore} plus the re-boot that arms the client as the Account, and
|
|
198
|
+
* whether the Connect landed. A refused exchange changes no identity, so it is not
|
|
199
|
+
* re-booted on: the answer would be the one already in hand.
|
|
200
|
+
*/
|
|
201
|
+
async function redeemConnectCode(code, gen) {
|
|
202
|
+
if (!(await exchangeAndStore(code, gen)))
|
|
203
|
+
return false;
|
|
204
|
+
await bootAndArm(gen);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Finish a Connect that started outside the app: read the one-time code off an
|
|
209
|
+
* inbound deep link and redeem it. Ignores a code it has already seen — iOS and
|
|
210
|
+
* Android both replay the launch URL in ordinary situations (a cold start whose
|
|
211
|
+
* `getInitialURL` is then re-delivered as a `url` event), and a code is single-use,
|
|
212
|
+
* so a second exchange would fail and report a Connect that in fact succeeded as one
|
|
213
|
+
* that did not. A declared `returnUrl` additionally narrows this to links on the
|
|
214
|
+
* app's own return origin; an app that declared none still redeems, because the
|
|
215
|
+
* claim page mints its code against the **Project's** app link, not against `init`.
|
|
216
|
+
*/
|
|
217
|
+
async function handleInboundUrl(url, gen, { reboot = true } = {}) {
|
|
218
|
+
try {
|
|
219
|
+
const code = (0, connect_1.parseInboundConnectCode)(url, options.returnUrl);
|
|
220
|
+
if (!code || code === s.lastInboundCode)
|
|
221
|
+
return;
|
|
222
|
+
s.lastInboundCode = code;
|
|
223
|
+
const landed = reboot
|
|
224
|
+
? await redeemConnectCode(code, gen)
|
|
225
|
+
: await exchangeAndStore(code, gen);
|
|
226
|
+
// A `signIn()` that went through the browser door is waiting on exactly this —
|
|
227
|
+
// the code it sent the person out for, arriving back on the app's own link.
|
|
228
|
+
if (landed)
|
|
229
|
+
settleInboundWaiters(true);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// A Connect problem must never disturb the host app.
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Listen for Connect deep links that arrive **while the app runs** — the person was
|
|
237
|
+
* already in the app (or it was backgrounded) when they tapped the claim page's
|
|
238
|
+
* button. Armed on the `linking` seam alone: a Project's app link is allowlisted on
|
|
239
|
+
* the Project, so an app that never declared a `returnUrl` in `init` is still where
|
|
240
|
+
* a claim lands (ADR-0046). Silent on an app that provides no seam, which can never
|
|
241
|
+
* be a Connect target.
|
|
242
|
+
*/
|
|
243
|
+
function armConnectLinking(gen) {
|
|
244
|
+
const linking = adapters.linking;
|
|
245
|
+
if (!linking)
|
|
246
|
+
return;
|
|
247
|
+
try {
|
|
248
|
+
const subscription = linking.addEventListener("url", (event) => {
|
|
249
|
+
void handleInboundUrl(event?.url ?? "", gen);
|
|
250
|
+
});
|
|
251
|
+
s.teardownLinking = () => subscription.remove();
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
// A Linking seam that refuses to subscribe is not a reason to fail a start.
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Redeem the code on the URL the app was **launched** with, before the first boot —
|
|
259
|
+
* the invite journey's cold start (the person tapped the claim page's redirect and
|
|
260
|
+
* the app opened). Awaited rather than left in flight, so the one boot that follows
|
|
261
|
+
* carries the Reporter session: two concurrent boots race, and an anonymous answer
|
|
262
|
+
* landing last leaves a Connect that succeeded looking like one that never happened.
|
|
263
|
+
* The browser SDK exchanges a URL code before booting for the same reason.
|
|
264
|
+
*
|
|
265
|
+
* Best-effort throughout: a seam that throws, a launch URL that carries nothing, or
|
|
266
|
+
* an exchange the server refuses all fall through to an ordinary anonymous boot.
|
|
267
|
+
*/
|
|
268
|
+
async function redeemLaunchUrl(gen) {
|
|
269
|
+
const linking = adapters.linking;
|
|
270
|
+
if (!linking)
|
|
271
|
+
return;
|
|
272
|
+
try {
|
|
273
|
+
const initial = await linking.getInitialURL();
|
|
274
|
+
if (gen !== s.generation || !initial)
|
|
275
|
+
return;
|
|
276
|
+
await handleInboundUrl(initial, gen, { reboot: false });
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
// Never throw into the host app.
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Revoke the Reporter session, clear it from SecureStore, and re-boot as anonymous —
|
|
284
|
+
* so a gated Project's launcher (shake gesture) drops at once. Never throws.
|
|
285
|
+
*/
|
|
286
|
+
async function signOut() {
|
|
287
|
+
const gen = s.generation;
|
|
288
|
+
try {
|
|
289
|
+
if (s.reporterSessionToken) {
|
|
290
|
+
await (0, sdk_core_1.revokeReporterSession)(apiUrl, s.reporterSessionToken, {
|
|
291
|
+
fetch: adapters.fetch,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
await (0, reporter_session_store_1.clearReporterSession)(adapters.secureStore, key);
|
|
295
|
+
s.reporterSessionToken = null;
|
|
296
|
+
s.identity = { ...s.identity, reporterSession: undefined };
|
|
297
|
+
if (gen === s.generation)
|
|
298
|
+
await bootAndArm(gen);
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
// A Fixback problem must never disturb the host app.
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return { signIn, signOut, armConnectLinking, redeemLaunchUrl, settleInboundWaiters };
|
|
305
|
+
}
|
|
306
|
+
//# sourceMappingURL=client-connect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-connect.js","sourceRoot":"","sources":["../src/client-connect.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mobile replay's arming (ADR-0044): whether the client records at all, the frame
|
|
3
|
+
* recorder it starts and stops on every boot, and turning a frozen window into the
|
|
4
|
+
* report's video.
|
|
5
|
+
*
|
|
6
|
+
* One piece of the headless client (`./client`), created over the state every piece
|
|
7
|
+
* shares (`./client-state`). The recorder and the window the composer froze live
|
|
8
|
+
* there, because the composer (`present`, `submit`) and boot (`./client-boot`) both
|
|
9
|
+
* reach them.
|
|
10
|
+
*/
|
|
11
|
+
import { type CaptureConfig } from "@fixback/sdk-core";
|
|
12
|
+
import type { ClientState } from "./client-state";
|
|
13
|
+
import { type MobileReplayWindow } from "./replay";
|
|
14
|
+
import { type ReplayAttachment } from "./replay-encode";
|
|
15
|
+
/** The replay half of capture arming, as {@link createReplayArming} builds it. */
|
|
16
|
+
export interface ReplayArming {
|
|
17
|
+
/** Start (or stop) the frame recorder for the Project's served config. */
|
|
18
|
+
armReplay(served: CaptureConfig | undefined): void;
|
|
19
|
+
/** Stop the recorder and drop every frame it owns (best-effort). */
|
|
20
|
+
disarmReplay(): void;
|
|
21
|
+
/** Delete a replay frame or encoded window file (best-effort, repeat-safe). */
|
|
22
|
+
releaseReplayFile(uri: string): void;
|
|
23
|
+
/** Give a frozen window back to the recorder, so its frames can be reclaimed. */
|
|
24
|
+
releaseReplayWindow(window: MobileReplayWindow | null): void;
|
|
25
|
+
/** Freeze the window as it stands now and encode it — the auto-capture path's replay. */
|
|
26
|
+
freezeAndEncodeReplay(): Promise<ReplayAttachment | null>;
|
|
27
|
+
/** Encode a frozen window for transport; `null` whenever it cannot become a video. */
|
|
28
|
+
encodeReplay(window: MobileReplayWindow | null): Promise<ReplayAttachment | null>;
|
|
29
|
+
}
|
|
30
|
+
/** Mobile replay's arming over the client's shared state. */
|
|
31
|
+
export declare function createReplayArming(s: ClientState): ReplayArming;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Mobile replay's arming (ADR-0044): whether the client records at all, the frame
|
|
4
|
+
* recorder it starts and stops on every boot, and turning a frozen window into the
|
|
5
|
+
* report's video.
|
|
6
|
+
*
|
|
7
|
+
* One piece of the headless client (`./client`), created over the state every piece
|
|
8
|
+
* shares (`./client-state`). The recorder and the window the composer froze live
|
|
9
|
+
* there, because the composer (`present`, `submit`) and boot (`./client-boot`) both
|
|
10
|
+
* reach them.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.createReplayArming = createReplayArming;
|
|
14
|
+
const sdk_core_1 = require("@fixback/sdk-core");
|
|
15
|
+
const replay_1 = require("./replay");
|
|
16
|
+
const replay_encode_1 = require("./replay-encode");
|
|
17
|
+
/** Mobile replay's arming over the client's shared state. */
|
|
18
|
+
function createReplayArming(s) {
|
|
19
|
+
const { options, adapters, warn, now, readEnvironment } = s;
|
|
20
|
+
/**
|
|
21
|
+
* Whether this app asked for mobile replay. Unlike every other stream, replay is
|
|
22
|
+
* **off until asked for** (ADR-0044): it records the device screen unmasked, so
|
|
23
|
+
* shipping an SDK update must never start it. Either spelling opts in —
|
|
24
|
+
* `capture: { replay: true }`, or simply passing a `replay` tuning object — and an
|
|
25
|
+
* explicit `capture: { replay: false }` always wins.
|
|
26
|
+
*/
|
|
27
|
+
function replayRequested() {
|
|
28
|
+
if (typeof options.capture?.replay === "boolean")
|
|
29
|
+
return options.capture.replay;
|
|
30
|
+
return options.replay !== undefined;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Whether the server turned mobile replay off: the Project's replay toggle (the master kill
|
|
34
|
+
* switch, #138), or — on a server that sends it — `mobileReplay`, that toggle under the
|
|
35
|
+
* platform's `mobile_replay` feature flag for the workspace (#647). An older server sends only
|
|
36
|
+
* `replay`, which is then read as before.
|
|
37
|
+
*/
|
|
38
|
+
function servedOff(served) {
|
|
39
|
+
if (typeof served?.replay === "boolean" && !served.replay)
|
|
40
|
+
return "project";
|
|
41
|
+
if (typeof served?.mobileReplay === "boolean" && !served.mobileReplay)
|
|
42
|
+
return "platform";
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Whether replay records: this app asked for it, the server has not turned it off, and the
|
|
47
|
+
* platform can actually capture and encode frames.
|
|
48
|
+
*/
|
|
49
|
+
function replayEnabled(served) {
|
|
50
|
+
if (!replayRequested())
|
|
51
|
+
return false;
|
|
52
|
+
if (servedOff(served) !== null)
|
|
53
|
+
return false;
|
|
54
|
+
return Boolean(adapters.captureReplayFrame && adapters.encodeReplaySegment);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Start (or stop) the frame recorder for the Project's served config. Re-armed on
|
|
58
|
+
* every boot, so a Project that turns replay off stops recording on the next boot
|
|
59
|
+
* rather than at the next launch — and the frames already buffered are deleted with
|
|
60
|
+
* the recorder, so nothing captured under the old answer can ship.
|
|
61
|
+
*/
|
|
62
|
+
function armReplay(served) {
|
|
63
|
+
disarmReplay();
|
|
64
|
+
if (!replayEnabled(served)) {
|
|
65
|
+
const off = servedOff(served);
|
|
66
|
+
if (replayRequested() && off === "project") {
|
|
67
|
+
warn(`${sdk_core_1.WARN_PREFIX} replay is enabled in init but turned off for this Project — ` +
|
|
68
|
+
"no replay will be recorded until it is enabled in the dashboard.");
|
|
69
|
+
}
|
|
70
|
+
else if (replayRequested() && off === "platform") {
|
|
71
|
+
warn(`${sdk_core_1.WARN_PREFIX} replay is enabled in init but mobile replay isn't available for this ` +
|
|
72
|
+
"workspace yet — no replay will be recorded.");
|
|
73
|
+
}
|
|
74
|
+
else if (replayRequested() && !adapters.encodeReplaySegment) {
|
|
75
|
+
warn(`${sdk_core_1.WARN_PREFIX} replay is enabled in init but no native encoder is linked — ` +
|
|
76
|
+
"mobile replay needs a development build (`npx expo prebuild`), not Expo Go.");
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
s.replayRecorder = (0, replay_1.startMobileReplayRecorder)(options.replay ?? {}, {
|
|
81
|
+
captureFrame: (quality) => adapters.captureReplayFrame(quality),
|
|
82
|
+
releaseFrame: (uri) => releaseReplayFile(uri),
|
|
83
|
+
isActive: adapters.isAppActive,
|
|
84
|
+
now,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/** Stop the recorder and drop every frame it owns (best-effort). */
|
|
88
|
+
function disarmReplay() {
|
|
89
|
+
try {
|
|
90
|
+
s.replayRecorder?.stop();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
/* teardown is best-effort */
|
|
94
|
+
}
|
|
95
|
+
s.replayRecorder = null;
|
|
96
|
+
s.pendingReplayWindow = null;
|
|
97
|
+
}
|
|
98
|
+
/** Delete a replay frame or encoded window file (best-effort, repeat-safe). */
|
|
99
|
+
function releaseReplayFile(uri) {
|
|
100
|
+
try {
|
|
101
|
+
adapters.releaseReplayFrame?.(uri);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
/* release is best-effort */
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** Give a frozen window back to the recorder, so its frames can be reclaimed. */
|
|
108
|
+
function releaseReplayWindow(window) {
|
|
109
|
+
if (!window)
|
|
110
|
+
return;
|
|
111
|
+
try {
|
|
112
|
+
s.replayRecorder?.release(window);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
/* release is best-effort */
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Freeze the window as it stands **now** and encode it — the auto-capture path's
|
|
120
|
+
* whole replay step (ADR-0044): an uncaught error has no composer to open, so the
|
|
121
|
+
* throw itself is the moment the window is taken. The window is given back as soon
|
|
122
|
+
* as the encode is done, since nothing holds a draft for an auto report.
|
|
123
|
+
*/
|
|
124
|
+
async function freezeAndEncodeReplay() {
|
|
125
|
+
const window = s.replayRecorder?.snapshot() ?? null;
|
|
126
|
+
if (!window)
|
|
127
|
+
return null;
|
|
128
|
+
try {
|
|
129
|
+
return await encodeReplay(window);
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
releaseReplayWindow(window);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Encode a frozen window for transport (ADR-0044). `null` whenever the window
|
|
137
|
+
* cannot become a video — no recorder, nothing watchable buffered, or an encoder
|
|
138
|
+
* that declined — so the report ships without replay rather than not at all.
|
|
139
|
+
*/
|
|
140
|
+
async function encodeReplay(window) {
|
|
141
|
+
if (!window || !adapters.encodeReplaySegment)
|
|
142
|
+
return null;
|
|
143
|
+
return (0, replay_encode_1.encodeReplayWindow)(window, {
|
|
144
|
+
encodeSegment: adapters.encodeReplaySegment,
|
|
145
|
+
windowSize: () => {
|
|
146
|
+
const env = readEnvironment();
|
|
147
|
+
return {
|
|
148
|
+
width: env.windowWidth ?? 0,
|
|
149
|
+
height: env.windowHeight ?? 0,
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
maxEdge: (0, replay_1.resolvedMaxEdge)(options.replay),
|
|
153
|
+
now,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
armReplay,
|
|
158
|
+
disarmReplay,
|
|
159
|
+
releaseReplayFile,
|
|
160
|
+
releaseReplayWindow,
|
|
161
|
+
freezeAndEncodeReplay,
|
|
162
|
+
encodeReplay,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=client-replay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-replay.js","sourceRoot":"","sources":["../src/client-replay.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The state the headless client's pieces share (#661): one explicit object, where
|
|
3
|
+
* `createFixbackClient` used to be a single closure whose parts reached into each
|
|
4
|
+
* other's variables.
|
|
5
|
+
*
|
|
6
|
+
* It holds what the client resolves from its options once, at creation — the trimmed
|
|
7
|
+
* key, the canonical origin, the API base, the validated Release, the Correlation id's
|
|
8
|
+
* targets, the trace buffer, the listener sets — and every value that changes over the
|
|
9
|
+
* client's life: the status and the boot answer, the lifecycle generation, the Connect
|
|
10
|
+
* state, the armed capture, and maintenance mode's notice. Every piece is created over
|
|
11
|
+
* the same object — boot and capture arming (`./client-boot`), mobile replay
|
|
12
|
+
* (`./client-replay`), Connect (`./client-connect`), and the composer-facing surface
|
|
13
|
+
* (`./client`) — so what one piece writes is what the next one reads.
|
|
14
|
+
*/
|
|
15
|
+
import { type BootAnswer, type BreadcrumbBuffer, type FetchLike, type IdentityInputs, type MaintenanceNotice, type MaintenanceRecheck, type ReporterIdentity, type ReporterTier, type Teardown } from "@fixback/sdk-core";
|
|
16
|
+
import type { AutoCaptureHandle } from "./error-capture";
|
|
17
|
+
import type { FormDataFactory } from "./http";
|
|
18
|
+
import type { ComposerContext, FixbackAdapters, FixbackOptions, FixbackStatus } from "./options";
|
|
19
|
+
import type { EnvironmentInputs } from "./report";
|
|
20
|
+
import type { MobileReplayRecorder, MobileReplayWindow } from "./replay";
|
|
21
|
+
/** Everything a client knows and holds — see the module doc. */
|
|
22
|
+
export interface ClientState {
|
|
23
|
+
readonly options: FixbackOptions;
|
|
24
|
+
readonly adapters: FixbackAdapters;
|
|
25
|
+
/** The publishable key, trimmed; `""` when there is none, which keeps the client dormant. */
|
|
26
|
+
readonly key: string;
|
|
27
|
+
/** The canonical `origin` the app reports as, or `""` when it declared none (ADR-0041). */
|
|
28
|
+
readonly origin: string;
|
|
29
|
+
/** The Fixback API base: trimmed, with no trailing slash. */
|
|
30
|
+
readonly apiUrl: string;
|
|
31
|
+
readonly now: () => number;
|
|
32
|
+
readonly readEnvironment: () => EnvironmentInputs;
|
|
33
|
+
/** The development-only warning sink (silent unless the adapters provide one). */
|
|
34
|
+
readonly warn: (message: string) => void;
|
|
35
|
+
/** The host app's Release (#117, ADR-0024), validated; `undefined` when unset or invalid. */
|
|
36
|
+
readonly release: string | undefined;
|
|
37
|
+
/** The Correlation id's targets (#540): the `propagateTo` entries that are bare origins. */
|
|
38
|
+
readonly propagateTo: readonly string[];
|
|
39
|
+
/** The trace buffer. It outlives every re-arm, so no lead-up is lost to one. */
|
|
40
|
+
readonly buffer: BreadcrumbBuffer;
|
|
41
|
+
readonly statusListeners: Set<(status: FixbackStatus) => void>;
|
|
42
|
+
readonly presentListeners: Set<(context: ComposerContext) => void>;
|
|
43
|
+
readonly identityListeners: Set<(identity: ReporterIdentity) => void>;
|
|
44
|
+
readonly maintenanceListeners: Set<(notice: MaintenanceNotice | null) => void>;
|
|
45
|
+
status: FixbackStatus;
|
|
46
|
+
boot: BootAnswer | null;
|
|
47
|
+
/** Monotonic id of the most recently *started* boot — see `bootAndArm` (`./client-boot`). */
|
|
48
|
+
bootSequence: number;
|
|
49
|
+
currentScreen: string | null;
|
|
50
|
+
teardownTrace: Teardown | null;
|
|
51
|
+
autoCapture: AutoCaptureHandle | null;
|
|
52
|
+
/**
|
|
53
|
+
* The mobile replay recorder, and the window frozen when the composer opened
|
|
54
|
+
* (ADR-0044). Both are null unless the Project enabled replay *and* this app asked
|
|
55
|
+
* for it — recording an unmasked screen is never something the SDK starts on its own.
|
|
56
|
+
*/
|
|
57
|
+
replayRecorder: MobileReplayRecorder | null;
|
|
58
|
+
pendingReplayWindow: MobileReplayWindow | null;
|
|
59
|
+
identity: IdentityInputs;
|
|
60
|
+
reporterSessionToken: string | null;
|
|
61
|
+
account: {
|
|
62
|
+
name: string;
|
|
63
|
+
email: string;
|
|
64
|
+
} | null;
|
|
65
|
+
currentTier: ReporterTier | null;
|
|
66
|
+
connectUrl: string | undefined;
|
|
67
|
+
/** Removes the inbound deep-link listener; re-armed by each `start()`. */
|
|
68
|
+
teardownLinking: Teardown | null;
|
|
69
|
+
/**
|
|
70
|
+
* The last inbound Connect code redeemed, so a replayed launch URL is not exchanged
|
|
71
|
+
* twice. A code is single-use: the second attempt would fail, and a failed exchange
|
|
72
|
+
* is indistinguishable from a Connect that never worked.
|
|
73
|
+
*/
|
|
74
|
+
lastInboundCode: string | null;
|
|
75
|
+
/**
|
|
76
|
+
* Callers waiting for the next inbound Connect to land — the browser door's
|
|
77
|
+
* `signIn()` (ADR-0052), which hands control to the connect page and gets it back as
|
|
78
|
+
* a deep link the inbound listener redeems. Resolved with `true` by a successful
|
|
79
|
+
* redemption, `false` when the wait gives up or the client is destroyed, so an
|
|
80
|
+
* awaited `signIn()` reports the identity the round trip actually produced instead
|
|
81
|
+
* of the anonymous one it started from.
|
|
82
|
+
*/
|
|
83
|
+
inboundWaiters: Array<(landed: boolean) => void>;
|
|
84
|
+
/**
|
|
85
|
+
* The lifecycle generation. Each `start()` claims a new generation and every
|
|
86
|
+
* await inside it re-checks that it is still current; `destroy()` bumps the
|
|
87
|
+
* generation so an in-flight start abandons itself. A destroyed client can be
|
|
88
|
+
* started again — React 18+ StrictMode (and Fast Refresh) replay the
|
|
89
|
+
* provider's effect as mount → cleanup → re-mount on the same client
|
|
90
|
+
* instance, so destroy must not be terminal.
|
|
91
|
+
*/
|
|
92
|
+
generation: number;
|
|
93
|
+
/** Whether a start() has claimed the current generation. */
|
|
94
|
+
running: boolean;
|
|
95
|
+
/** Maintenance mode's notice — boot's, or a held report's — else `null`. */
|
|
96
|
+
maintenance: MaintenanceNotice | null;
|
|
97
|
+
/** The next boot while the notice shows. */
|
|
98
|
+
readonly maintenanceRecheck: MaintenanceRecheck;
|
|
99
|
+
/**
|
|
100
|
+
* The last dormancy line printed, so a re-boot that changes nothing (a `signOut`
|
|
101
|
+
* back to the same refusal, an app resumed) does not repeat itself. Reset whenever
|
|
102
|
+
* the client becomes able to submit, so a *later* relapse is reported again.
|
|
103
|
+
*/
|
|
104
|
+
lastDormancyWarning: string | null;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Resolve a client's options once, saying what it cannot use as it goes, and set up its
|
|
108
|
+
* state at rest: `idle`, nothing armed, no boot answer yet.
|
|
109
|
+
*/
|
|
110
|
+
export declare function createClientState(options: FixbackOptions, adapters: FixbackAdapters): ClientState;
|
|
111
|
+
/** Move the client to `next`, telling every status listener once. */
|
|
112
|
+
export declare function setStatus(s: ClientState, next: FixbackStatus): void;
|
|
113
|
+
/**
|
|
114
|
+
* The `url` a report carries right now (spec 0004 §D). With an `origin` configured
|
|
115
|
+
* it reads `<origin>/<screen>`; without one — the default since ADR-0041 — the
|
|
116
|
+
* screen stands alone, so the Evidence still says *where* the reporter was rather
|
|
117
|
+
* than losing it to a setting nobody has to fill in. Ingest takes `url` as a free
|
|
118
|
+
* string and the dashboard renders it as text, so a bare screen name is honest
|
|
119
|
+
* here where a synthesised origin would be fiction.
|
|
120
|
+
*/
|
|
121
|
+
export declare function currentUrl(s: ClientState): string | undefined;
|
|
122
|
+
/** The transport deps every call shares: the injected seams plus this app's origin. */
|
|
123
|
+
export declare function submitDeps(s: ClientState): {
|
|
124
|
+
fetch?: FetchLike;
|
|
125
|
+
formData?: FormDataFactory;
|
|
126
|
+
origin: string;
|
|
127
|
+
};
|
|
128
|
+
/** The current Reporter identity for the composer's identity row and `identity()`. */
|
|
129
|
+
export declare function reporterIdentity(s: ClientState): ReporterIdentity;
|
|
130
|
+
/** Tell identity listeners the identity may have changed (boot / sign in / sign out). */
|
|
131
|
+
export declare function notifyIdentity(s: ClientState): void;
|