@seekprotocol/game 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +20 -2
- package/bridge.d.ts +115 -0
- package/collector.d.ts +67 -0
- package/diagnostics.d.ts +233 -0
- package/index.d.ts +5 -0
- package/index.js +982 -0
- package/package.json +25 -4
- package/perf.d.ts +43 -0
- package/protocol.d.ts +99 -0
- package/seek-game.js +982 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Seek Protocol (https://www.seekprotocol.ai/en)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @seekprotocol/game
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The GameKit bridge between a minigame and the Seek app. Made by
|
|
4
|
+
[Seek Protocol](https://www.seekprotocol.ai/en).
|
|
5
|
+
|
|
6
|
+
A minigame in a quest is a web page that Seek shows inside the app. This bridge
|
|
7
|
+
tells the app when the game is ready, reports the result and sends Seekprotocol
|
|
8
|
+
Vitals (start time, frame rate, errors) so makers see how their game runs on
|
|
9
|
+
real phones.
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { createBridge } from '@seekprotocol/game';
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`seek-game.js` is the same bridge as one file for a game without a bundler.
|
|
16
|
+
The bridge is readable on purpose: it ships inside every game and App Review
|
|
17
|
+
reads it.
|
|
18
|
+
|
|
19
|
+
Seek Protocol: https://www.seekprotocol.ai/en
|
|
20
|
+
|
|
21
|
+
Copyright (c) 2026 Seek Protocol. MIT licensed, see `LICENSE.md`: ship the bridge with your game freely.
|
package/bridge.d.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bridge a minigame imports: `ready()`, `launch()`, `finish()`,
|
|
3
|
+
* `storage` and `track`.
|
|
4
|
+
*
|
|
5
|
+
* Two hosts are possible. In the Seek app the game is the top page of a
|
|
6
|
+
* hardened WebView: the bridge posts to `window.ReactNativeWebView` and the app
|
|
7
|
+
* answers by dispatching `message` events on the page (no source window). In
|
|
8
|
+
* Store Connect's simulator the game sits in an iframe: the bridge posts to
|
|
9
|
+
* its parent, only to the allowed host origins, and accepts answers only from
|
|
10
|
+
* the parent window at the origin that answered `ready`.
|
|
11
|
+
*
|
|
12
|
+
* The bridge never sees who the player is. The launch token names a
|
|
13
|
+
* pseudonymous `sub`, the run, the seed and the language; it is what the
|
|
14
|
+
* proxy accepts for storage, track and Vitals, and nothing else.
|
|
15
|
+
*
|
|
16
|
+
* Seekprotocol Vitals: once ready, the bridge reports uncaught errors and
|
|
17
|
+
* unhandled rejections, the game's own `log()` lines (and, if the game opts
|
|
18
|
+
* in, console output), a heartbeat and the session's end. Everything is
|
|
19
|
+
* scrubbed, batched and limited first (see `collector.ts` and `diagnostics.ts`).
|
|
20
|
+
*/
|
|
21
|
+
import { type HostEventType, type LaunchInfo, type TrackProps } from './protocol.js';
|
|
22
|
+
import { type CollectorTimers } from './collector.js';
|
|
23
|
+
import { type LogLevel } from './diagnostics.js';
|
|
24
|
+
export declare class BridgeError extends Error {
|
|
25
|
+
readonly code: string;
|
|
26
|
+
constructor(code: string, message: string);
|
|
27
|
+
}
|
|
28
|
+
interface MessageEventLike {
|
|
29
|
+
data: unknown;
|
|
30
|
+
origin: string;
|
|
31
|
+
source: unknown;
|
|
32
|
+
}
|
|
33
|
+
type Listener = (e: MessageEventLike) => void;
|
|
34
|
+
/** What the bridge needs of `window`; tests pass a fake. Besides `message` it listens for `error`, `unhandledrejection` and `pagehide`. */
|
|
35
|
+
export interface WindowLike {
|
|
36
|
+
addEventListener(type: string, fn: (e: any) => void): void;
|
|
37
|
+
removeEventListener(type: string, fn: (e: any) => void): void;
|
|
38
|
+
parent?: {
|
|
39
|
+
postMessage(message: unknown, targetOrigin: string): void;
|
|
40
|
+
} | null;
|
|
41
|
+
ReactNativeWebView?: {
|
|
42
|
+
postMessage(message: string): void;
|
|
43
|
+
};
|
|
44
|
+
document?: {
|
|
45
|
+
addEventListener(type: 'message', fn: Listener): void;
|
|
46
|
+
removeEventListener(type: 'message', fn: Listener): void;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export interface BridgeOptions {
|
|
50
|
+
window?: WindowLike;
|
|
51
|
+
/** Frame hosts allowed to answer. Default: Store Connect. Add `http://localhost:<port>` for a local simulator. */
|
|
52
|
+
hostOrigins?: readonly string[];
|
|
53
|
+
fetch?: typeof fetch;
|
|
54
|
+
/** How long a request waits for the host, in ms. */
|
|
55
|
+
timeoutMs?: number;
|
|
56
|
+
/** Seekprotocol Vitals. On by default; `false` turns off error capture, heartbeats and log(). */
|
|
57
|
+
vitals?: VitalsOptions | false;
|
|
58
|
+
}
|
|
59
|
+
type ConsoleLike = Partial<Record<'debug' | 'log' | 'info' | 'warn' | 'error', (...args: unknown[]) => void>>;
|
|
60
|
+
export interface VitalsOptions {
|
|
61
|
+
/** Capture uncaught errors and unhandled promise rejections. Default true. */
|
|
62
|
+
errors?: boolean;
|
|
63
|
+
/** Also send console output: true for every level, or a list such as `['warn', 'error']`. Default off. */
|
|
64
|
+
console?: boolean | readonly LogLevel[];
|
|
65
|
+
/** Seconds between heartbeats, 5 to 25. Default 10. */
|
|
66
|
+
heartbeatSeconds?: number;
|
|
67
|
+
/** Seconds between sends of a non-empty queue. Default 5. */
|
|
68
|
+
flushSeconds?: number;
|
|
69
|
+
/** Measure frames per second, jank, the heap and the time to ready(). Default true. */
|
|
70
|
+
performance?: boolean;
|
|
71
|
+
/** For tests. */
|
|
72
|
+
consoleObject?: ConsoleLike;
|
|
73
|
+
timers?: CollectorTimers;
|
|
74
|
+
now?: () => number;
|
|
75
|
+
}
|
|
76
|
+
export interface FinishResult {
|
|
77
|
+
/** Whether the host took the score to Seek. Whether it counts is Seek's decision. */
|
|
78
|
+
accepted: boolean;
|
|
79
|
+
}
|
|
80
|
+
export interface Bridge {
|
|
81
|
+
/** The handshake. Resolves with the launch once the host answered. Safe to call twice. */
|
|
82
|
+
ready(): Promise<LaunchInfo>;
|
|
83
|
+
/** The run, after `ready()`. */
|
|
84
|
+
launch(): LaunchInfo;
|
|
85
|
+
/** Report the run, once. The score is advisory: Seek checks it against the step and the run ticket. */
|
|
86
|
+
finish(result: {
|
|
87
|
+
score: number;
|
|
88
|
+
seconds: number;
|
|
89
|
+
}): Promise<FinishResult>;
|
|
90
|
+
storage: {
|
|
91
|
+
get(key: string): Promise<unknown>;
|
|
92
|
+
set(key: string, value: unknown): Promise<void>;
|
|
93
|
+
remove(key: string): Promise<void>;
|
|
94
|
+
};
|
|
95
|
+
/** Count an event. Never throws; returns false when the event was dropped. */
|
|
96
|
+
track(event: string, props?: TrackProps): boolean;
|
|
97
|
+
/**
|
|
98
|
+
* A line for Seekprotocol Vitals, shown to you in Store Connect. Never throws;
|
|
99
|
+
* false when it was dropped (not ready yet beyond 20 lines, over the limits, or Vitals are off).
|
|
100
|
+
* Do not log personal data: emails, positions, long numbers and tokens are scrubbed, but the rest is kept for 30 days.
|
|
101
|
+
*/
|
|
102
|
+
log(level: LogLevel, message: string, data?: unknown): boolean;
|
|
103
|
+
/** End the session cleanly (the player left the game) and send what is left. The bridge keeps working for storage and finish. */
|
|
104
|
+
exit(): Promise<void>;
|
|
105
|
+
/** The Vitals session id of this launch, after ready(): search for it in Store Connect's logs. */
|
|
106
|
+
sessionId(): string | null;
|
|
107
|
+
/** Host events: the app went to the background (`pause`), came back (`resume`), or is closing the game (`exit`). */
|
|
108
|
+
on(event: HostEventType, fn: () => void): () => void;
|
|
109
|
+
/** Stop listening. */
|
|
110
|
+
close(): void;
|
|
111
|
+
}
|
|
112
|
+
/** Events per launch the bridge sends at most; the proxy counts too. */
|
|
113
|
+
export declare const TRACK_PER_LAUNCH = 200;
|
|
114
|
+
export declare function createBridge(opts?: BridgeOptions): Bridge;
|
|
115
|
+
export {};
|
package/collector.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bridge's side of Seekprotocol Vitals: a queue of scrubbed entries that
|
|
3
|
+
* goes out in small batches, within per-launch limits and a rate limit.
|
|
4
|
+
*
|
|
5
|
+
* It knows nothing about windows or transports: the bridge feeds it errors
|
|
6
|
+
* and logs and gives it a `send` that posts to the proxy or the host.
|
|
7
|
+
*/
|
|
8
|
+
import { type DiagnosticsBatch, type EndReason, type ErrorSource, type LogLevel } from './diagnostics.js';
|
|
9
|
+
export interface CollectorTimers {
|
|
10
|
+
setInterval(fn: () => void, ms: number): unknown;
|
|
11
|
+
clearInterval(t: unknown): void;
|
|
12
|
+
}
|
|
13
|
+
export interface CollectorOptions {
|
|
14
|
+
/** Deliver one batch. `final` is the last one of the session (send it with keepalive). Errors are swallowed. */
|
|
15
|
+
send: (batch: DiagnosticsBatch, final: boolean) => Promise<unknown> | unknown;
|
|
16
|
+
/** A heartbeat for the host's hang check, every `heartbeatMs`, and once with `ended` at the end. */
|
|
17
|
+
beat?: (ended: boolean) => void;
|
|
18
|
+
/** Exact texts never to send (the launch token). */
|
|
19
|
+
secrets?: () => readonly string[];
|
|
20
|
+
now?: () => number;
|
|
21
|
+
timers?: CollectorTimers;
|
|
22
|
+
/** How often a non-empty queue is sent. Default 5 s. */
|
|
23
|
+
flushMs?: number;
|
|
24
|
+
/** How often a heartbeat goes out. Default 10 s, at least 5 s, at most 25 s (the host's hang limit is 30 s). */
|
|
25
|
+
heartbeatMs?: number;
|
|
26
|
+
/** Entries per second on average, and the burst. */
|
|
27
|
+
rate?: {
|
|
28
|
+
perSecond: number;
|
|
29
|
+
burst: number;
|
|
30
|
+
};
|
|
31
|
+
/** What each heartbeat carries about how smooth the game runs (perf.ts). */
|
|
32
|
+
sample?: () => {
|
|
33
|
+
fps?: number;
|
|
34
|
+
jank?: number;
|
|
35
|
+
heap_mb?: number;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export interface ErrorInput {
|
|
39
|
+
source: ErrorSource;
|
|
40
|
+
message: string;
|
|
41
|
+
stack?: string;
|
|
42
|
+
file?: string;
|
|
43
|
+
line?: number;
|
|
44
|
+
col?: number;
|
|
45
|
+
}
|
|
46
|
+
export interface Collector {
|
|
47
|
+
/** The session started (after ready): sends what was buffered before, then runs the timers. `readyMs`: navigation to ready(). */
|
|
48
|
+
start(session: string, readyMs?: number): void;
|
|
49
|
+
error(e: ErrorInput): boolean;
|
|
50
|
+
log(level: LogLevel, message: string, data?: unknown, source?: 'log' | 'console'): boolean;
|
|
51
|
+
/** End the session once and send what is left. */
|
|
52
|
+
end(reason: EndReason): Promise<void>;
|
|
53
|
+
flush(final?: boolean): Promise<void>;
|
|
54
|
+
/** For tests and the docs: what the collector counted. */
|
|
55
|
+
stats(): {
|
|
56
|
+
sent: number;
|
|
57
|
+
dropped: number;
|
|
58
|
+
errors: number;
|
|
59
|
+
logs: number;
|
|
60
|
+
batches: number;
|
|
61
|
+
};
|
|
62
|
+
readonly session: string | null;
|
|
63
|
+
readonly ended: boolean;
|
|
64
|
+
}
|
|
65
|
+
/** Entries kept before ready(); the rest are dropped and counted. */
|
|
66
|
+
export declare const PRE_READY_ENTRIES = 20;
|
|
67
|
+
export declare function createCollector(opts: CollectorOptions): Collector;
|
package/diagnostics.d.ts
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seekprotocol Vitals: the shapes, limits, scrubbing and grouping of a
|
|
3
|
+
* minigame's crash and log reports.
|
|
4
|
+
*
|
|
5
|
+
* The bridge collects errors, logs, heartbeats and the session's start and
|
|
6
|
+
* end, scrubs them and sends them in small batches: to the proxy
|
|
7
|
+
* (`POST /v1/diagnostics`, with the launch token) or, in the simulator, to the
|
|
8
|
+
* host. A host (the Seek app, the simulator) adds what only it can see: a game
|
|
9
|
+
* that never became ready, stopped answering, lost its WebView process or did
|
|
10
|
+
* not load (`HostEvent`). The proxy runs every check here again and scrubs
|
|
11
|
+
* again: nothing the game sends is trusted.
|
|
12
|
+
*
|
|
13
|
+
* No dependencies and no DOM, so the Workers and the hosts import this file too.
|
|
14
|
+
*/
|
|
15
|
+
export declare const LOG_LEVELS: readonly ["debug", "info", "warn", "error"];
|
|
16
|
+
export type LogLevel = (typeof LOG_LEVELS)[number];
|
|
17
|
+
export declare const isLogLevel: (v: unknown) => v is LogLevel;
|
|
18
|
+
/** What the host reports for a game it runs. Each one ends the session as a crash. */
|
|
19
|
+
export declare const HOST_EVENTS: readonly ["no_ready", "hang", "render_gone", "load_failed"];
|
|
20
|
+
export type HostCrashEvent = (typeof HOST_EVENTS)[number];
|
|
21
|
+
export declare const isHostEvent: (v: unknown) => v is HostCrashEvent;
|
|
22
|
+
/** A host reports `no_ready` when the game has not called ready() this long after it started loading. */
|
|
23
|
+
export declare const HOST_READY_TIMEOUT_MS = 10000;
|
|
24
|
+
/** A host reports `hang` when a ready game sent no heartbeat for this long while in the foreground. */
|
|
25
|
+
export declare const HOST_HANG_MS = 30000;
|
|
26
|
+
export declare const HOST_EVENT_TEXT: Record<HostCrashEvent, string>;
|
|
27
|
+
/** How a session ended. `exit`, `pagehide` and `closed` come from the bridge; the rest from the host. */
|
|
28
|
+
export declare const END_REASONS: readonly ["exit", "pagehide", "closed", "host_exit", "no_ready", "hang", "render_gone", "load_failed"];
|
|
29
|
+
export type EndReason = (typeof END_REASONS)[number];
|
|
30
|
+
/** Where an error came from. `error` and `unhandledrejection` are uncaught and count as crashes. */
|
|
31
|
+
export declare const ERROR_SOURCES: readonly ["error", "unhandledrejection", "console", "manual"];
|
|
32
|
+
export type ErrorSource = (typeof ERROR_SOURCES)[number];
|
|
33
|
+
export declare const isFatalSource: (s: ErrorSource) => s is "error" | "unhandledrejection";
|
|
34
|
+
export declare const DIAG_LIMITS: {
|
|
35
|
+
/** Characters of a message (log or error). */
|
|
36
|
+
readonly message: 500;
|
|
37
|
+
/** Characters of a stack, after scrubbing. */
|
|
38
|
+
readonly stack: 4000;
|
|
39
|
+
/** Stack frames kept. */
|
|
40
|
+
readonly frames: 20;
|
|
41
|
+
/** Characters of a source file path. */
|
|
42
|
+
readonly file: 200;
|
|
43
|
+
/** UTF-8 bytes of a log's data as JSON. */
|
|
44
|
+
readonly data: 2048;
|
|
45
|
+
/** Characters of a host event's detail. */
|
|
46
|
+
readonly detail: 200;
|
|
47
|
+
/** Entries in one batch. */
|
|
48
|
+
readonly batchEntries: 50;
|
|
49
|
+
/** UTF-8 bytes of one batch as JSON (the proxy reads at most 64 KB). */
|
|
50
|
+
readonly batchBytes: number;
|
|
51
|
+
/** Error entries per session; the proxy keeps the same limit. */
|
|
52
|
+
readonly sessionErrors: 100;
|
|
53
|
+
/** Log entries per session. */
|
|
54
|
+
readonly sessionLogs: 1000;
|
|
55
|
+
/** One error entry stands for at most this many identical errors. */
|
|
56
|
+
readonly repeat: 1000;
|
|
57
|
+
};
|
|
58
|
+
export declare const DIAG_BODY_MAX_BYTES: number;
|
|
59
|
+
export interface ErrorEntry {
|
|
60
|
+
kind: 'error';
|
|
61
|
+
/** ms since 1970, the device's clock. The proxy clamps it. */
|
|
62
|
+
at: number;
|
|
63
|
+
source: ErrorSource;
|
|
64
|
+
message: string;
|
|
65
|
+
stack?: string;
|
|
66
|
+
file?: string;
|
|
67
|
+
line?: number;
|
|
68
|
+
col?: number;
|
|
69
|
+
/** How many identical errors this entry stands for. */
|
|
70
|
+
count?: number;
|
|
71
|
+
}
|
|
72
|
+
export interface LogEntry {
|
|
73
|
+
kind: 'log';
|
|
74
|
+
at: number;
|
|
75
|
+
level: LogLevel;
|
|
76
|
+
message: string;
|
|
77
|
+
data?: unknown;
|
|
78
|
+
/** `console` when captured from console.*, else the game called log(). */
|
|
79
|
+
source?: 'log' | 'console';
|
|
80
|
+
}
|
|
81
|
+
/** A heartbeat, with how the game ran since the last one (perf.ts): frames per second, frames over 50 ms, the JS heap in MB. */
|
|
82
|
+
export interface HeartbeatEntry {
|
|
83
|
+
kind: 'heartbeat';
|
|
84
|
+
at: number;
|
|
85
|
+
fps?: number;
|
|
86
|
+
jank?: number;
|
|
87
|
+
heap_mb?: number;
|
|
88
|
+
}
|
|
89
|
+
/** The session's start; `ready_ms` is the time from navigation to ready(). */
|
|
90
|
+
export interface StartEntry {
|
|
91
|
+
kind: 'start';
|
|
92
|
+
at: number;
|
|
93
|
+
ready_ms?: number;
|
|
94
|
+
}
|
|
95
|
+
/** The ranges a measurement must fall in; anything else is left out. */
|
|
96
|
+
export declare const PERF_RANGES: {
|
|
97
|
+
readonly fps: readonly [0, 240];
|
|
98
|
+
readonly jank: readonly [0, 100000];
|
|
99
|
+
readonly heap_mb: readonly [0, 65536];
|
|
100
|
+
readonly ready_ms: readonly [0, 600000];
|
|
101
|
+
};
|
|
102
|
+
export interface EndEntry {
|
|
103
|
+
kind: 'end';
|
|
104
|
+
at: number;
|
|
105
|
+
reason: EndReason;
|
|
106
|
+
}
|
|
107
|
+
/** Only a host may send these, with a host token. */
|
|
108
|
+
export interface HostEntry {
|
|
109
|
+
kind: 'host';
|
|
110
|
+
at: number;
|
|
111
|
+
event: HostCrashEvent;
|
|
112
|
+
detail?: string;
|
|
113
|
+
}
|
|
114
|
+
export type DiagnosticEntry = ErrorEntry | LogEntry | HeartbeatEntry | StartEntry | EndEntry | HostEntry;
|
|
115
|
+
/** What the bridge sends, to the proxy or to the host. Who and what game come from the token, never from here. */
|
|
116
|
+
export interface DiagnosticsBatch {
|
|
117
|
+
/** The session id the bridge derived (`sessionIdOf`); the proxy derives its own and ignores this. */
|
|
118
|
+
session?: string;
|
|
119
|
+
/** Batch number within the session, from 1. */
|
|
120
|
+
seq?: number;
|
|
121
|
+
entries: DiagnosticEntry[];
|
|
122
|
+
/** Entries the bridge left out because of its own limits since the last batch. */
|
|
123
|
+
dropped?: number;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The launch context a host knows about the device: never free text from the
|
|
127
|
+
* game. platform-quest signs it into the launch token (`ctx`) from what the
|
|
128
|
+
* app sent to `run_start`.
|
|
129
|
+
*/
|
|
130
|
+
export interface LaunchContext {
|
|
131
|
+
device: 'phone' | 'tablet' | 'desktop' | 'unknown';
|
|
132
|
+
os: 'ios' | 'android' | 'web' | 'unknown';
|
|
133
|
+
/** `17.4`, `14`: digits and dots only. */
|
|
134
|
+
os_version?: string;
|
|
135
|
+
/** The Seek app's version, `3.2.0`, or `simulator`. */
|
|
136
|
+
app_version?: string;
|
|
137
|
+
}
|
|
138
|
+
export declare const DEVICE_CLASSES: readonly LaunchContext['device'][];
|
|
139
|
+
export declare const OS_NAMES: readonly LaunchContext['os'][];
|
|
140
|
+
/** Why a launch context is not one, or null. */
|
|
141
|
+
export declare function launchContextProblem(c: unknown): string | null;
|
|
142
|
+
/** The context as stored: anything missing or odd becomes `unknown` / null. */
|
|
143
|
+
export declare function normaliseContext(c: unknown): {
|
|
144
|
+
device: string;
|
|
145
|
+
os: string;
|
|
146
|
+
os_version: string | null;
|
|
147
|
+
app_version: string | null;
|
|
148
|
+
};
|
|
149
|
+
/** A text with emails, tokens, keys, positions, long numbers and URL queries taken out. */
|
|
150
|
+
export declare function scrubText(s: string, extra?: readonly string[]): string;
|
|
151
|
+
/** Plain JSON with every text scrubbed and keys that name personal or secret things emptied. Null when it is not JSON. */
|
|
152
|
+
export declare function scrubData(value: unknown, extra?: readonly string[], depth?: number): unknown;
|
|
153
|
+
/** Data as stored: scrubbed, and replaced by a marker when it is larger than the limit. */
|
|
154
|
+
export declare function boundData(value: unknown, extra?: readonly string[]): unknown;
|
|
155
|
+
/**
|
|
156
|
+
* A game's own URLs, shortened to their path: `https://<app>.play.seekprotocol.ai/@<hash>/game.js`
|
|
157
|
+
* becomes `/game.js`, so stacks read well and group across versions.
|
|
158
|
+
*/
|
|
159
|
+
export declare function shortenGameUrls(s: string): string;
|
|
160
|
+
export interface StackFrame {
|
|
161
|
+
fn: string;
|
|
162
|
+
file: string;
|
|
163
|
+
line: number | null;
|
|
164
|
+
col: number | null;
|
|
165
|
+
}
|
|
166
|
+
/** The frames of a V8, JavaScriptCore or Gecko stack, top first. */
|
|
167
|
+
export declare function parseStack(stack: string | undefined): StackFrame[];
|
|
168
|
+
/**
|
|
169
|
+
* A message with what changes between occurrences taken out: numbers, quoted
|
|
170
|
+
* texts, hex ids and placeholders the scrubber left. Two errors that differ
|
|
171
|
+
* only there are one group.
|
|
172
|
+
*/
|
|
173
|
+
export declare function normaliseMessage(message: string): string;
|
|
174
|
+
/** How many frames name a group. */
|
|
175
|
+
export declare const FINGERPRINT_FRAMES = 3;
|
|
176
|
+
/** The part of a frame that stays the same across builds: the file and the function, or the line when it has no name. */
|
|
177
|
+
export declare const frameKey: (f: StackFrame) => string;
|
|
178
|
+
export declare function sha256HexOf(s: string): Promise<string>;
|
|
179
|
+
/** The text a fingerprint hashes: the kind, the normalised message and the top frames. */
|
|
180
|
+
export declare function fingerprintInput(kind: string, message: string, stack?: string, file?: string, line?: number): string;
|
|
181
|
+
/** A group id: the first 32 hex of SHA-256 over `fingerprintInput`. */
|
|
182
|
+
export declare function fingerprint(kind: string, message: string, stack?: string, file?: string, line?: number): Promise<string>;
|
|
183
|
+
/** The session id of a launch: the same in the bridge, the proxy and Store Connect, and not the run id itself. */
|
|
184
|
+
export declare function sessionIdOf(appId: string, runId: string): Promise<string>;
|
|
185
|
+
export interface ParsedBatch {
|
|
186
|
+
entries: DiagnosticEntry[];
|
|
187
|
+
/** What the bridge says it dropped before sending (its own limits). */
|
|
188
|
+
dropped: number;
|
|
189
|
+
/** Entries that were not valid and were left out. */
|
|
190
|
+
invalid: number;
|
|
191
|
+
/** Host entries in a batch that did not come from a host. */
|
|
192
|
+
hostOnly: number;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Check a batch and clean every entry: unknown kinds and broken shapes are
|
|
196
|
+
* dropped (counted in `invalid`), texts are clipped and scrubbed again, stacks
|
|
197
|
+
* are shortened. `host` entries are only kept when `from` is `host`.
|
|
198
|
+
*/
|
|
199
|
+
export declare function parseDiagnosticsBatch(body: unknown, opts?: {
|
|
200
|
+
from: 'game' | 'host';
|
|
201
|
+
secrets?: readonly string[];
|
|
202
|
+
}): ParsedBatch | null;
|
|
203
|
+
/** One entry, checked, clipped and scrubbed, or null. The bridge runs it before sending, the proxy again on receipt. */
|
|
204
|
+
export declare function cleanDiagnosticEntry(e: unknown, secrets?: readonly string[]): DiagnosticEntry | null;
|
|
205
|
+
/** Whether an entry ends its session as a crash. */
|
|
206
|
+
export declare const isCrashEntry: (e: DiagnosticEntry) => boolean;
|
|
207
|
+
export interface WatchdogOptions {
|
|
208
|
+
/** Called once with the crash the host saw; the host then sends it (app) or shows it (simulator). */
|
|
209
|
+
onCrash: (entry: HostEntry) => void;
|
|
210
|
+
readyTimeoutMs?: number;
|
|
211
|
+
hangMs?: number;
|
|
212
|
+
now?: () => number;
|
|
213
|
+
setTimer?: (fn: () => void, ms: number) => unknown;
|
|
214
|
+
clearTimer?: (t: unknown) => void;
|
|
215
|
+
}
|
|
216
|
+
export interface HostWatchdog {
|
|
217
|
+
/** The game answered ready. */
|
|
218
|
+
ready(): void;
|
|
219
|
+
/** A heartbeat (or any message) arrived from the game. */
|
|
220
|
+
beat(): void;
|
|
221
|
+
/** The app went to the background (no hang while paused) or came back. */
|
|
222
|
+
pause(): void;
|
|
223
|
+
resume(): void;
|
|
224
|
+
/** The WebView reported a failure: `render_gone` or `load_failed`. */
|
|
225
|
+
fail(event: 'render_gone' | 'load_failed', detail?: string): void;
|
|
226
|
+
/** The game ended cleanly, or the host closed it: stop watching. */
|
|
227
|
+
stop(): void;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* The timers a host needs for `no_ready` and `hang`. Start it when the game
|
|
231
|
+
* page starts loading; it reports at most one crash, then stops.
|
|
232
|
+
*/
|
|
233
|
+
export declare function createHostWatchdog(opts: WatchdogOptions): HostWatchdog;
|