@typetorch/framework 0.3.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +124 -12
- package/out/analytics/SCHEMA.md +13 -6
- package/out/analytics/engine.luau +1 -1
- package/out/analytics/hints.d.ts +36 -0
- package/out/analytics/hints.luau +190 -0
- package/out/analytics/queue.d.ts +5 -0
- package/out/analytics/queue.luau +10 -0
- package/out/analytics/schema.d.ts +21 -9
- package/out/analytics/schema.luau +1 -7
- package/out/analytics/server.d.ts +15 -1
- package/out/analytics/server.luau +175 -112
- package/out/analytics/settings.d.ts +10 -3
- package/out/analytics/settings.luau +61 -20
- package/out/analytics/status.d.ts +5 -0
- package/out/analytics/status.luau +30 -0
- package/out/budget.d.ts +7 -0
- package/out/budget.luau +27 -0
- package/out/detached.d.ts +26 -0
- package/out/detached.luau +95 -0
- package/out/devtools/admin-server.luau +40 -55
- package/out/devtools/assets-ui.d.ts +2 -2
- package/out/devtools/assets-ui.luau +2 -2
- package/out/devtools/budget-ui.d.ts +15 -0
- package/out/devtools/budget-ui.luau +186 -0
- package/out/devtools/claude.luau +20 -9
- package/out/devtools/client.luau +70 -22
- package/out/devtools/health.d.ts +57 -4
- package/out/devtools/health.luau +504 -23
- package/out/devtools/roll-call.d.ts +11 -6
- package/out/devtools/roll-call.luau +11 -6
- package/out/devtools/server.luau +18 -1
- package/out/devtools/widgets.d.ts +8 -0
- package/out/devtools/widgets.luau +14 -1
- package/out/index.d.ts +8 -1
- package/out/init.luau +6 -0
- package/out/kernel.d.ts +393 -4
- package/out/kernel.luau +65 -1
- package/out/messaging.d.ts +74 -0
- package/out/messaging.luau +479 -0
- package/out/module.d.ts +9 -0
- package/out/module.luau +2 -0
- package/out/net/compat.d.ts +147 -0
- package/out/net/compat.luau +440 -0
- package/out/net/index.d.ts +11 -2
- package/out/net/init.luau +26 -3
- package/out/net/limits.d.ts +11 -0
- package/out/net/limits.luau +25 -0
- package/out/net/runtime.d.ts +15 -2
- package/out/net/runtime.luau +76 -9
- package/out/net/types.d.ts +20 -1
- package/out/runtime/dependency.d.ts +79 -0
- package/out/runtime/dependency.luau +240 -0
- package/out/runtime/player-state.d.ts +30 -0
- package/out/runtime/player-state.luau +183 -0
- package/out/runtime/relay.d.ts +15 -0
- package/out/runtime/relay.luau +58 -0
- package/out/runtime/start.luau +119 -40
- package/out/servers.d.ts +70 -0
- package/out/servers.luau +367 -0
- package/out/settings.d.ts +49 -0
- package/out/settings.luau +236 -0
- package/out/typetorch.d.ts +83 -2
- package/out/typetorch.luau +91 -4
- package/out/version.d.ts +1 -1
- package/out/version.luau +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,16 +57,25 @@ swap. It needs the TypeTorch kernel in the place (`@typetorch/kernel`).
|
|
|
57
57
|
|
|
58
58
|
### Coming from Flamework
|
|
59
59
|
|
|
60
|
-
Game code only changes imports: `@Service`, `@Controller`, constructor injection, the lifecycle
|
|
61
|
-
`createNetwork` are the same.
|
|
60
|
+
Game code only changes imports: `@Service`, `@Controller`, constructor injection, `Dependency<T>()`, the lifecycle
|
|
61
|
+
interfaces and `createNetwork` are the same.
|
|
62
62
|
|
|
63
63
|
| Flamework | TypeTorch |
|
|
64
64
|
|---|---|
|
|
65
65
|
| `rbxts-transformer-flamework` plugin, `node_modules/@flamework` type root | `@typetorch/transformer` plugin, `node_modules/@typetorch` type root |
|
|
66
66
|
| `import { Modding, Reflect } from "@flamework/core"` | `import { Modding, Reflect } from "@typetorch/framework"` |
|
|
67
|
+
| `import { Dependency } from "@flamework/core"` | `import { Dependency } from "@typetorch/framework"` (from onInit/onStart on, not in constructors) |
|
|
68
|
+
| `import type` + `Dependency<T>()` to break a cycle | the same, or `Lazy<T>` (below) |
|
|
69
|
+
| `ClientEvents.x.predict(...)` | `network.client.x.emit(...)` |
|
|
70
|
+
| `invokeWithTimeout(timeout, ...)` | `invokeWithTimeout(seconds, ...)` (seconds, 0.5 to 120) |
|
|
71
|
+
| `Observers.observeCharacter` / `observeLocalCharacter` (@rbxts/observers) | still fine: put the stop function in the trove, `this.trove.add(observeCharacter(...))` |
|
|
67
72
|
| `import { t } from "@rbxts/t"` (still fine) | also `import { t } from "@typetorch/framework"` |
|
|
68
73
|
| `@metadata flamework:parameters` keys, `"flamework:parameters"` metadata | `@metadata typetorch:parameters`, `"typetorch:parameters"` |
|
|
69
74
|
| `flamework.build`, `include/flamework`, the `@flamework` Rojo mapping | gone; delete them |
|
|
75
|
+
| `Networking.createEvent` / `createFunction` and their call sites | `createFlameworkCompat` (below) keeps every call site; `createNetwork` is the native API |
|
|
76
|
+
|
|
77
|
+
`typetorch migrate --from flamework` (CLI) does most of this, and the docs' `guides/from-flamework.md` has the whole
|
|
78
|
+
mapping.
|
|
70
79
|
|
|
71
80
|
## Usage
|
|
72
81
|
|
|
@@ -99,6 +108,19 @@ export class CoinService extends Module implements OnStart {
|
|
|
99
108
|
- **Modules:** `@Service()` (server) and `@Controller()` (client) classes extending `Module`. Constructor parameters
|
|
100
109
|
are other modules, injected by type. Lifecycle: `OnInit` (sequential, dependencies first), `OnStart` (spawned),
|
|
101
110
|
`OnStop` (reverse order), `OnTick`, `OnPhysics`, `OnRender`, `OnPlayerAdded` (replays players already in the server).
|
|
111
|
+
- **Modules outside the constructor:** `Dependency<T>()` (or `TypeTorch.module<T>()`) returns the running module of
|
|
112
|
+
type T from anywhere: a method, a plain class a module built, a command handler. `TypeTorch.tryModule<T>()` returns
|
|
113
|
+
undefined instead of throwing. They work once every module is constructed (from onInit on); in a constructor, a
|
|
114
|
+
field initializer or module top-level code they throw `"ShopService isn't constructed yet: ..."`. Only this realm's
|
|
115
|
+
modules, and per generation: after a swap they return the new generation's instance. An `import type` of the class is
|
|
116
|
+
enough (no require cycle).
|
|
117
|
+
- **Cycles:** two modules that need each other take one side as `Lazy<T>`, resolved on first use:
|
|
118
|
+
`constructor(private readonly team: Lazy<TeamService>)` (needs `@typetorch/transformer` 0.2.1+) or a field,
|
|
119
|
+
`private readonly team = Lazy<TeamService>()` (any transformer). `this.team.get()` works from onInit on (in a
|
|
120
|
+
constructor it throws), and the start order ignores lazy edges. A cycle error names the cycle and suggests this.
|
|
121
|
+
- **Per-player state:** `this.ctx.playerState("cooldowns.v1", (player) => init)` (or `TypeTorch.playerState`) returns
|
|
122
|
+
`{ get, set, has, delete }` keyed by UserId. It survives swaps (persist), and a player's entry goes when they really
|
|
123
|
+
leave, never on a swap. The dev menu shows it under Modules > State > persist, `__playerState`.
|
|
102
124
|
- **Bad deploys roll back (kernel 0.3.2):** a server `onStart` that throws is reported to the kernel, which rolls the
|
|
103
125
|
server back to its last known good artifact when it happens within 30 s of start (so do 3 errors from the new
|
|
104
126
|
code's scripts in that time). On a real shutdown every module's `onStop` runs too (the kernel's BindToClose).
|
|
@@ -106,6 +128,18 @@ export class CoinService extends Module implements OnStart {
|
|
|
106
128
|
behind. `TypeTorch.persist(key, init)` (or `this.ctx.persist`) keeps plain data across swaps.
|
|
107
129
|
- **Network:** `createNetwork<C2S, S2C>()` with nested namespaces. The server checks rate limits, shape limits and the
|
|
108
130
|
generated type guard on every client message. `setNetworkLimits({ "chat.say": { maxString: 200 } })` tunes a leaf.
|
|
131
|
+
Requests wait 15 s for the answer: `invokeWithTimeout(5, ...args)` sets it per call, and a leaf's `timeout`
|
|
132
|
+
(`setNetworkLimits({ "vc.spawn": { timeout: 30 } })`, read by the client, so set it in shared code) per leaf; both in
|
|
133
|
+
seconds, 0.5 to 120. `network.client.x.emit(...args)` runs this client's own `on` handlers for a server -> client
|
|
134
|
+
leaf right away, with no traffic (Flamework's `predict`); `network.server.x.emit(player, ...args)` does the same on
|
|
135
|
+
the server, for tests. A request handler may return a Promise: the answer waits for it.
|
|
136
|
+
- **Flamework networking names:** `createFlameworkCompat<C2SEvents, S2CEvents, C2SFunctions, S2CFunctions>()` returns
|
|
137
|
+
`{ ServerEvents, ClientEvents, ServerFunctions, ClientFunctions, GlobalEvents, GlobalFunctions }` with
|
|
138
|
+
`@flamework/networking` 1.x's methods (`connect` returning a connection, `fire(player | players)`, `broadcast`,
|
|
139
|
+
`except`, `predict`, `setCallback`, `invoke`, `invokeWithTimeout`, calling a leaf directly) on this same network:
|
|
140
|
+
same guards, limits and swap rules, so a Flamework game's call sites compile unchanged. Listeners run on their own
|
|
141
|
+
thread each, like Flamework's, and belong to the running generation (no trove needed). Not carried over: server ->
|
|
142
|
+
client requests, middleware, `Networking.Unreliable`. `typetorch build` notes a game still using it.
|
|
109
143
|
Across a swap (0.3.0): what the server sends a player waits until that player's client runs the new generation
|
|
110
144
|
(reliable messages are queued, unreliable ones dropped), and on kernel 0.3.2 a request the server can no longer
|
|
111
145
|
answer fails at once ("The game is updating, try again.") instead of timing out.
|
|
@@ -167,6 +201,10 @@ leaves one behind. Each `on*` returns a disconnect function, which a trove can o
|
|
|
167
201
|
TypeTorch.artifact; // { id, commit, commitHash, branch, channel, builtAt, seq, assetId }
|
|
168
202
|
TypeTorch.generation; // 1, 2, 3... per server (or client)
|
|
169
203
|
TypeTorch.branch; TypeTorch.channel; TypeTorch.serverType; // "public" | "private" | "reserved" | "studio"
|
|
204
|
+
// Kernel 0.3.9: `channel` is the RULES this server runs under ("prod" on every public server; split data stores by it);
|
|
205
|
+
// `branchChannel` is what the branch IS ("prod" for the default branch or one configured prod, else "dev");
|
|
206
|
+
// `rules` is the same as `channel`, by name.
|
|
207
|
+
TypeTorch.branchChannel; TypeTorch.rules;
|
|
170
208
|
TypeTorch.isPinned(); TypeTorch.kernelVersion; TypeTorch.kernelApi; TypeTorch.jobId; TypeTorch.isStudio;
|
|
171
209
|
|
|
172
210
|
// How this generation started: a branch change starts a new generation.
|
|
@@ -182,6 +220,13 @@ this.trove.add(TypeTorch.onUpdatePending((update) => (hint.Visible = !update.can
|
|
|
182
220
|
|
|
183
221
|
// State that survives swaps (plain data only)
|
|
184
222
|
const scores = TypeTorch.persist("scores.v1", () => new Map<number, number>());
|
|
223
|
+
// Per player, removed when the player really leaves (same as this.ctx.playerState)
|
|
224
|
+
const cooldowns = TypeTorch.playerState("cooldowns.v1", () => ({ lastUse: 0 }));
|
|
225
|
+
cooldowns.get(player).lastUse = os.clock();
|
|
226
|
+
|
|
227
|
+
// The running module of a type (same as Dependency<T>()), from onInit/onStart on
|
|
228
|
+
TypeTorch.module<ShopService>().open(player);
|
|
229
|
+
TypeTorch.tryModule<ShopService>()?.open(player); // undefined instead of an error
|
|
185
230
|
|
|
186
231
|
// Dev and roles (server: the kernel decides; client: only the local player, cosmetic)
|
|
187
232
|
if (TypeTorch.isOwner(player)) showOwnerPanel(player);
|
|
@@ -198,6 +243,23 @@ TypeTorch.onLog((entry) => errors.push(entry)); // don't print from inside it
|
|
|
198
243
|
|
|
199
244
|
// Hot assets (below): same as hotAsset(...)
|
|
200
245
|
const shop = TypeTorch.asset("ui/shop");
|
|
246
|
+
|
|
247
|
+
// Live values, server only (kernel 0.3.8): the signed settings record's `game` field (`typetorch settings set game.<key> <json>`).
|
|
248
|
+
const price = TypeTorch.liveConfig("shop.price", { default: 50, parse: (raw) => (typeIs(raw, "number") ? raw : 50) });
|
|
249
|
+
price.get();
|
|
250
|
+
this.trove.add(price.onChanged((value) => this.reprice(value)));
|
|
251
|
+
|
|
252
|
+
// Cross-server, server only. Kernel 0.3.8: game topics over ONE kernel-held MessagingService topic; listeners hear
|
|
253
|
+
// prod servers and this branch by default (dev branches never reach prod); publish queues and retries, max 1 KiB.
|
|
254
|
+
this.trove.add(TypeTorch.messaging.subscribe<Ban>("1guard", (ban, meta) => applyBan(ban, meta.jobId)));
|
|
255
|
+
TypeTorch.messaging.publish("1guard", { kind: "ban", userId }, { to: "prod" });
|
|
256
|
+
|
|
257
|
+
// The universe's live servers (the roll call; cached per server, yields up to ~3 s when stale) and this server's public fields
|
|
258
|
+
TypeTorch.setServerInfo({ region: "eu", vc: true }); // JSON, at most 400 bytes, kept across swaps
|
|
259
|
+
const list = TypeTorch.servers(); // [{ jobId, placeVersion, players, maxPlayers, serverType, branch, channel, uptime, here, info }]
|
|
260
|
+
|
|
261
|
+
// A library call a swap must not cut off halfway (kernel 0.3.8): runs on a kernel thread; write what must survive into persist.
|
|
262
|
+
TypeTorch.runDetached(() => store.StartSessionAsync(`Player_${userId}`)).then((profile) => attach(player, profile));
|
|
201
263
|
```
|
|
202
264
|
|
|
203
265
|
- **`startInfo.reason`:** `boot`, `deploy`, `rollback`, `branch`, `pin`, `reload`, `server_rollback`,
|
|
@@ -214,6 +276,25 @@ const shop = TypeTorch.asset("ui/shop");
|
|
|
214
276
|
reason and timings, the next artifact in `onSwapOut`, `onUpdatePending`, server-side `onPlayerDevChanged` and
|
|
215
277
|
`requestReload`. On older kernels `startInfo` still knows boot vs swap, the previous artifact and branch (the
|
|
216
278
|
framework records them), `onBranchChanged` still fires, and the reason of a swap is `unknown`.
|
|
279
|
+
- **`messaging`** (kernel 0.3.8): `subscribe(topic, (data, meta) => ...)` returns a disconnect; `meta` = `{ channel,
|
|
280
|
+
branch, jobId, serverType, placeVersion, sentAt, self, replayed? }`; `{ from: "all" | "prod" | "branch" }` changes
|
|
281
|
+
who it hears. `publish(topic, data, { to })` throws on a bad topic, non-JSON data or a message over 1 KiB (with its
|
|
282
|
+
size), returns false when dropped at once (queue full). Studio loops back; the cloud test publishes nothing; older
|
|
283
|
+
kernels throw "needs kernel 0.3.8". `servers()` works on every kernel (0.3.8 holds the roll call topic, so a swap
|
|
284
|
+
doesn't subscribe again).
|
|
285
|
+
- **`liveConfig(key, { default, parse? })`** (kernel 0.3.8, plans/20): reads `game[key]` of the signed settings record the
|
|
286
|
+
kernel verified (signed by both prod keys, so game code can't forge it); `get()` and `onChanged(fn)` (only on real
|
|
287
|
+
changes; generation-scoped). The default without a record or key, when `parse` throws (warned per value), and on older
|
|
288
|
+
kernels (warned once: "needs kernel 0.3.8"; no ConfigService fallback). `TypeTorch.settings()` is the whole copy
|
|
289
|
+
(`seq`, `at`, `game`, `analytics`, `fleet`, ...): server only, it holds tokens.
|
|
290
|
+
- **`runDetached(fn)`** (kernel 0.3.8, server only): runs `fn` on a thread the kernel owns, so a deploy's hard stop
|
|
291
|
+
(which kills this generation's threads mid-call) can't cut it off: for ProfileStore / ProfileService loads, saves and
|
|
292
|
+
releases. Returns a Promise that settles while this generation runs; after a swap the result is dropped, so a job
|
|
293
|
+
whose result must survive writes it into `persist` itself. The job keeps this generation's closures alive until it
|
|
294
|
+
ends. Errors reject it and go to the kernel's log with the generation's name (never the health window); at most 256
|
|
295
|
+
run at once per server (then it throws); one past 60 s is logged and flagged in Server > Status. Older kernels
|
|
296
|
+
throw "needs kernel 0.3.8; use the DataHost job queue (Player data guide)": the other option, which works on every
|
|
297
|
+
kernel. `features.runDetached` tells which one this server has.
|
|
217
298
|
- **Edit mode** (UI Labs stories, no kernel): `running` is false, identity has defaults, `persist` keeps a local table,
|
|
218
299
|
events never fire, and the server-only reads throw.
|
|
219
300
|
|
|
@@ -227,6 +308,8 @@ pieces that keep it safe across swaps:
|
|
|
227
308
|
- **Session handles live in `persist`**, keyed by `UserId`. `onPlayerAdded` replays everyone after a swap, so it
|
|
228
309
|
re-attaches to the open session instead of loading again.
|
|
229
310
|
- **Release only on `Players.PlayerRemoving`**, never in `onStop` or `onSwapOut`.
|
|
311
|
+
- **Library calls can't be cut off by a swap:** run them as DataHost jobs (any kernel) or with
|
|
312
|
+
`TypeTorch.runDetached` (kernel 0.3.8).
|
|
230
313
|
- **Split store names by channel** (`TypeTorch.channel === "prod" ? "PlayerData" : "PlayerData_dev"`).
|
|
231
314
|
|
|
232
315
|
Full example with ProfileStore: [Player data guide](https://github.com/typetorch/docs/blob/main/guides/player-data.md).
|
|
@@ -287,7 +370,7 @@ self-hosted DuckDB analytics server). Nothing runs until an engine is created: n
|
|
|
287
370
|
```ts
|
|
288
371
|
import { AnalyticsEngine } from "@typetorch/framework";
|
|
289
372
|
|
|
290
|
-
// A server module (onInit):
|
|
373
|
+
// A server module (onInit): the sink comes from the signed settings' `analytics` (kernel 0.3.8).
|
|
291
374
|
const analytics = new AnalyticsEngine();
|
|
292
375
|
analytics.track(player, "quest_done", { quest: "tutorial" }); // custom
|
|
293
376
|
analytics.step(player, "onboarding", 3, "opened_shop"); // funnels
|
|
@@ -317,13 +400,13 @@ const variant = analytics.experiment("onboarding", ["short", "long"]); // same a
|
|
|
317
400
|
`TTZone`, named by a `Name` attribute or the instance name), `screens` (ScreenGuis in PlayerGui, GuiObjects tagged
|
|
318
401
|
`TTScreen`), `recording` (the first-ever session in detail), `fleet` (kernel 0.3.2 heartbeats and deploy reports),
|
|
319
402
|
`identity` (one `{ pid, uid }` row per session to the dev's own server, below). `settings` (server only) replaces the
|
|
320
|
-
|
|
403
|
+
signed settings' `analytics`, for tests, Studio and kernels before 0.3.8.
|
|
321
404
|
- **Every row** carries the time (server clock), a random player id (never the UserId), the session, the server
|
|
322
405
|
(JobId, type, place), the artifact (id, seq, branch, channel), the device, new vs returning, the player's state
|
|
323
406
|
(`zone:Lobby|screen:Shop|activity:round`) and experiment variants. The exact rows: `src/analytics/SCHEMA.md`.
|
|
324
407
|
- **Experiments:** `experiment(...)` is deterministic per player and name (they keep their variant in every session),
|
|
325
408
|
may yield briefly the first time (until the player's id loads), and stamps the variant on the player's later events.
|
|
326
|
-
The settings
|
|
409
|
+
The settings turn one off (`active: false`: everyone gets the first variant), sets `weights` or forces a
|
|
327
410
|
`variant`, live.
|
|
328
411
|
- **First-ever session:** for new players (a share of them, `recordShare`), the client records character and camera
|
|
329
412
|
about 10 times a second, every input (never while a TextBox or the chat has focus; text boxes only say which box was
|
|
@@ -333,7 +416,7 @@ const variant = analytics.experiment("onboarding", ["short", "long"]); // same a
|
|
|
333
416
|
row carries a UserId.
|
|
334
417
|
- **Identities** (option `identity`, default on): once the pid is known, one row `{ pid, uid, t }` (the UserId, nothing
|
|
335
418
|
else) per session goes to the dev's own server only, never into the events: duckdb games in the batch body, Basin
|
|
336
|
-
games to the fleet API (`
|
|
419
|
+
games to the fleet API (the settings' `fleet` url + `/v1/identity`, or the `identity` / `identityToken` settings). The
|
|
337
420
|
analytics server keeps pid -> UserId in a deletable table (support lookups, Right to Erasure).
|
|
338
421
|
- **Player ids:** DataStore `TypeTorchAnalytics`, key `p/<UserId>` -> `{ pid, first, last }`: one read per join, a
|
|
339
422
|
write on the first join and at leave. Deleting the key (Right to Erasure) leaves that player's rows anonymous.
|
|
@@ -341,6 +424,17 @@ const variant = analytics.experiment("onboarding", ["short", "long"]); // same a
|
|
|
341
424
|
`flushSeconds` (15) or at 500 rows, at most ~10 HttpService requests a minute, retries with backoff, and a last
|
|
342
425
|
flush on shutdown (kernel 0.3.2). Delivery is at least once (a swap mid-request sends that batch again).
|
|
343
426
|
`analytics.stats()` (server) has the counters; `flush()` sends soon.
|
|
427
|
+
- **When uploads fail** (a stale quick-tunnel URL, the dev PC's server or tunnel down, a wrong token, HttpService off;
|
|
428
|
+
Roblox reports most network problems as `HttpError: NetFail`, `DnsResolve` or `ConnectFail`): the server backs off
|
|
429
|
+
with jitter (5, 10, 20 ... 300 s, spread +-25% so a fleet of servers doesn't retry in step; a wrong token or URL waits
|
|
430
|
+
about 5 minutes, and a settings change retries at once), keeps the rows, and logs at most ONE line a minute: the
|
|
431
|
+
reason and the fix (`analytics upload failed 5 times in a row: NetFail: the connection broke mid-request. ... Retrying
|
|
432
|
+
in 80 s.`, with "(+N more in the last minute)" for the lines it held back) and one line when uploads work again.
|
|
433
|
+
`stats()` also has `failures` (in a row), `failed` (since the server started), `lastError`, `lastStatus`
|
|
434
|
+
(0 = no answer), `lastErrorAt` / `lastOkAt` (`os.time()`), `retryIn`, and, while failing, `reason` and `fix`. The dev
|
|
435
|
+
menu shows them: **Server > Status** has an *Analytics* line (queued, sent, `FAILING x5: ... retry in 80 s`) and a
|
|
436
|
+
*Fleet API* line (the kernel's sender: sent, failed, last error and its age), and **Attention** lists the reason and
|
|
437
|
+
the fix; `typetorch doctor` tests the address and token in the settings record.
|
|
344
438
|
- **The cloud test sends nothing.** `typetorch test --cloud` boots the payload headless in a Luau Execution task where
|
|
345
439
|
HttpService works; there the engine (stub kernel `test = true`, or `workspace:GetAttribute("TypeTorchTest")`) collects
|
|
346
440
|
as usual but never uploads: no event rows, no identity rows, no HTTP request, so a prod deploy never puts a fake
|
|
@@ -352,9 +446,17 @@ const variant = analytics.experiment("onboarding", ["short", "long"]); // same a
|
|
|
352
446
|
- **Friends in the server** (the join's `friends`): one `Players:GetFriendsAsync` per join (at most 10 pages, none
|
|
353
447
|
when the player is alone) intersected with the players there, not a web call per player.
|
|
354
448
|
|
|
355
|
-
**Settings
|
|
356
|
-
`
|
|
357
|
-
|
|
449
|
+
**Settings, framework 0.4.0 / kernel 0.4.0 (plans/21):** the record's `backend` = `{ url, key, analytics?: { flushSeconds,
|
|
450
|
+
recordShare } }` (`typetorch backend setup`) is the whole sink: DuckDB at `<url>/v1/ingest` with the one key (Basin
|
|
451
|
+
identities to `<url>/v1/identity`). The old `analytics` value below is read while `backend` is missing, and stays the
|
|
452
|
+
way to a Basin sink. The engine also tells the kernel each player's pid (`setAnalyticsId`: the kernel's error reports
|
|
453
|
+
count affected players by pid, never by name) and counts its own DataStore and HTTP requests for the budget view (dev
|
|
454
|
+
menu **Server > Budget**: bars per Roblox limit for the player count, who spends it, the DataStore budget left, memory).
|
|
455
|
+
|
|
456
|
+
**Settings** (server only, never sent to clients): the signed settings record's `analytics` field (kernel 0.3.8,
|
|
457
|
+
plans/20), written with `typetorch settings set analytics -` (JSON on stdin; `@typetorch/analytics`' `writeSettings()`
|
|
458
|
+
and `bun run local` call it). Every new copy applies live within seconds (the CLI pings servers). Before kernel 0.3.8
|
|
459
|
+
there are no settings unless the game passes `new AnalyticsEngine({ settings })`:
|
|
358
460
|
|
|
359
461
|
```json
|
|
360
462
|
{ "backend": "basin", "events": "https://<stream-id>.ingest.cloudflare.com",
|
|
@@ -392,10 +494,20 @@ bun run build # rbxtsc --type package -> out/
|
|
|
392
494
|
package.json.
|
|
393
495
|
`scripts/test-layout.luau` checks the dev menu's pane layout core (`devtools/layout.ts`: splits and their limits,
|
|
394
496
|
closing, pop out and dock, singleton pages, ratios, focus, and cleaning a remembered layout).
|
|
497
|
+
`scripts/test-modules.luau` runs the real `startServer`/`startClient` of `out/` (with RuntimeLib, Promise, trove and
|
|
498
|
+
t from node_modules) over several generations that share a persist store: `Dependency<T>()`, `TypeTorch.module` /
|
|
499
|
+
`tryModule` and their errors, `Lazy<T>` (parameters and fields, cycles, start order), network `emit` and request
|
|
500
|
+
timeouts and `playerState` (swaps, leaves).
|
|
501
|
+
`scripts/test-flamework-compat.luau` (same harness) drives `createFlameworkCompat` through each generation's
|
|
502
|
+
dispatcher: connect (one thread per call, guards, Disconnect, troves), fire / except / broadcast / the call
|
|
503
|
+
shorthand, setCallback with values and Promises, predict, a swap, and the client side (invoke, invokeWithTimeout,
|
|
504
|
+
`createClient({ defaultTimeout })`).
|
|
395
505
|
`scripts/test-analytics.luau` checks the analytics engine's pure parts (experiment assignment, settings, the queue
|
|
396
|
-
and HTTP budget, tt-rec-1, the sink request bodies); `test-analytics-server.luau` and
|
|
397
|
-
run the compiled server and client cores against mocked services (sessions, intake,
|
|
398
|
-
in flight, shutdown; the recorder,
|
|
506
|
+
and HTTP budget, tt-rec-1, the sink request bodies, the failure help); `test-analytics-server.luau` and
|
|
507
|
+
`test-analytics-client.luau` run the compiled server and client cores against mocked services (sessions, intake,
|
|
508
|
+
retries, a NetFail streak and its one log line a minute, a swap with a request in flight, shutdown; the recorder,
|
|
509
|
+
screens, batching). `scripts/test-net-health.luau` checks the dev menu's Fleet API / Analytics status lines and
|
|
510
|
+
Attention issues.
|
|
399
511
|
- **Publishing:** `npm publish` runs `prepublishOnly` (clean + build). The package ships only `out/` (no
|
|
400
512
|
`.tsbuildinfo`), `README.md` and `LICENSE`; check with `bun pm pack --dry-run`.
|
|
401
513
|
|
package/out/analytics/SCHEMA.md
CHANGED
|
@@ -128,8 +128,10 @@ start, i.e. the row's `t`). Time is the running sum of `dt`.
|
|
|
128
128
|
|
|
129
129
|
## Sink settings
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
`
|
|
131
|
+
The signed settings record's `analytics` field (kernel 0.3.8, plans/20; server-only; before 0.3.8 the ConfigService key
|
|
132
|
+
`TypeTorchAnalytics`), a JSON object, written with `typetorch settings set analytics -` (JSON on stdin; the analytics
|
|
133
|
+
repo's `bun run local` does it). Every new signed copy applies at once, so dials change live (the CLI pings servers).
|
|
134
|
+
`new AnalyticsEngine({ settings })` replaces it (tests, Studio, kernels before 0.3.8).
|
|
133
135
|
|
|
134
136
|
```json
|
|
135
137
|
{
|
|
@@ -154,8 +156,8 @@ ConfigService key `TypeTorchAnalytics` (server-only), a JSON object (or a JSON s
|
|
|
154
156
|
| `recordShare` | 1 | 0..1, share of new players recorded (stable per pid) |
|
|
155
157
|
| `techEvery` | 60 | 15..3600 seconds between tech samples |
|
|
156
158
|
| `experiments` | none | Per experiment: `active: false` (everyone gets the first variant, not stamped), `weights` (per variant, in the game's order), `variant` (force one) |
|
|
157
|
-
| `identity` | `
|
|
158
|
-
| `identityToken` | the `
|
|
159
|
+
| `identity` | the settings' `fleet` url + `/v1/identity` | basin: where identity rows go (below); none and no `fleet` in the settings: not sent. duckdb: unused |
|
|
160
|
+
| `identityToken` | the settings' `fleet` token | basin: the token for `identity`. Never logged |
|
|
159
161
|
|
|
160
162
|
Unknown fields are ignored. Settings errors are warned once (never with the token).
|
|
161
163
|
|
|
@@ -174,8 +176,13 @@ Unknown fields are ignored. Settings errors are warned once (never with the toke
|
|
|
174
176
|
- **Batches:** every `flushSeconds`, or at 500 queued events or 20 recordings. At most 500 event rows (fleet rows
|
|
175
177
|
first) and 50 recording rows per request, about 900 KB each. At most ~10 requests a minute per server (burst 3).
|
|
176
178
|
- **Answers:** 2xx sent; 413 the batch is halved and resent (a single row is dropped); 401/403/404 rows are kept and
|
|
177
|
-
sending waits 300 s (or until the settings change); 408/429/5xx/no answer retry with
|
|
178
|
-
other 4xx drop the batch (counted as rejected).
|
|
179
|
+
sending waits about 300 s (225-375 s, jittered; or until the settings change); 408/429/5xx/no answer retry with
|
|
180
|
+
backoff 5, 10, 20... 300 s (each +-25%); other 4xx drop the batch (counted as rejected).
|
|
181
|
+
- **Failure log and status:** at most one log line a minute (the reason and the fix from `hints.ts`, the streak, the next
|
|
182
|
+
try; later lines say how many were held back) and one line when uploads work again. `stats()`: `failures` (in a
|
|
183
|
+
row), `failed` (total), `lastError`, `lastStatus` (0: no answer, an `HttpError` such as NetFail / DnsResolve),
|
|
184
|
+
`lastErrorAt`, `lastOkAt`, `retryIn`, and while failing `reason` and `fix`. The dev menu reads them through
|
|
185
|
+
`analytics/status.ts`.
|
|
179
186
|
- **Delivery is at least once:** a hot swap during a request sends its rows again. Readers may drop exact duplicate
|
|
180
187
|
rows.
|
|
181
188
|
- **The cloud test sends nothing:** inside `typetorch test --cloud` (the stub kernel's `test = true`, or the workspace
|
|
@@ -10,7 +10,7 @@ local ServerAnalytics = TS.import(script, script.Parent, "server").ServerAnalyti
|
|
|
10
10
|
* client. Nothing runs until one is created. Every engine created in a generation shares one core, which stops with
|
|
11
11
|
* the generation; its unsent rows survive a hot swap in `persist`.
|
|
12
12
|
*
|
|
13
|
-
* The server sends rows to the sink in the settings (
|
|
13
|
+
* The server sends rows to the sink in the settings (the signed settings' `analytics`, kernel 0.3.8; or `options.settings`).
|
|
14
14
|
* The client sends everything through the server, never to the internet. See the README "Analytics" section and
|
|
15
15
|
* src/analytics/SCHEMA.md.
|
|
16
16
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a failed upload to the dev's analytics / fleet server means, and what to do about it. Shared by the analytics
|
|
3
|
+
* engine's log line (at most one a minute) and the dev menu (Status > Analytics / Fleet API, the "Attention" list), so
|
|
4
|
+
* both say the same thing.
|
|
5
|
+
*
|
|
6
|
+
* `HttpService.RequestAsync` throws a message with Roblox's HttpError name in it ("HttpError: NetFail"); a reply that
|
|
7
|
+
* isn't 2xx has a status. The names that matter to a dev running the server on their own PC behind a Cloudflare quick
|
|
8
|
+
* tunnel (`bun run local`):
|
|
9
|
+
*
|
|
10
|
+
* NetFail the connection broke mid-request (curl send/receive error, nothing usable came back): the server or
|
|
11
|
+
* tunnel restarted, dropped it, or is overloaded. The usual one when the dev PC's tunnel flaps.
|
|
12
|
+
* DnsResolve the host name doesn't resolve: the quick tunnel's URL died with its cloudflared (a new run gets a new
|
|
13
|
+
* URL) and the settings still hold the old one.
|
|
14
|
+
* ConnectFail nothing accepted the connection.
|
|
15
|
+
* TimedOut it accepted but never answered.
|
|
16
|
+
* (HTTP 530/1033 a tunnel with nothing behind it, 502/504 a tunnel whose server is down, 401/403 a wrong token, 404 a
|
|
17
|
+
* wrong path.)
|
|
18
|
+
*
|
|
19
|
+
* Pure: no imports and no services, so it also runs offline under Lune (scripts/test-net-health.luau). Nothing here
|
|
20
|
+
* ever sees or returns a token.
|
|
21
|
+
*/
|
|
22
|
+
export interface FailureHelp {
|
|
23
|
+
/** What happened, in a few words ("the connection broke mid-request"). */
|
|
24
|
+
reason: string;
|
|
25
|
+
/** What to do about it, one or two sentences. */
|
|
26
|
+
fix: string;
|
|
27
|
+
}
|
|
28
|
+
/** The Roblox HttpError name in an error message ("HttpError: NetFail" -> "NetFail"), or undefined. */
|
|
29
|
+
export declare function httpErrorName(message: string | undefined): string | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Help for a failed request. `status` is the HTTP status (0: no answer at all) and `message` the error text (from the
|
|
32
|
+
* thrown HttpError, or "HTTP 530" style text for a status; either may be missing).
|
|
33
|
+
*/
|
|
34
|
+
export declare function describeFailure(status: number, message?: string): FailureHelp;
|
|
35
|
+
/** Help for a kernel Fleet sender error text: "HTTP 530", or an HttpError message (`status().fleet.lastError`). */
|
|
36
|
+
export declare function describeFleetError(lastError: string | undefined): FailureHelp | undefined;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
--[[
|
|
3
|
+
*
|
|
4
|
+
* What a failed upload to the dev's analytics / fleet server means, and what to do about it. Shared by the analytics
|
|
5
|
+
* engine's log line (at most one a minute) and the dev menu (Status > Analytics / Fleet API, the "Attention" list), so
|
|
6
|
+
* both say the same thing.
|
|
7
|
+
*
|
|
8
|
+
* `HttpService.RequestAsync` throws a message with Roblox's HttpError name in it ("HttpError: NetFail"); a reply that
|
|
9
|
+
* isn't 2xx has a status. The names that matter to a dev running the server on their own PC behind a Cloudflare quick
|
|
10
|
+
* tunnel (`bun run local`):
|
|
11
|
+
*
|
|
12
|
+
* NetFail the connection broke mid-request (curl send/receive error, nothing usable came back): the server or
|
|
13
|
+
* tunnel restarted, dropped it, or is overloaded. The usual one when the dev PC's tunnel flaps.
|
|
14
|
+
* DnsResolve the host name doesn't resolve: the quick tunnel's URL died with its cloudflared (a new run gets a new
|
|
15
|
+
* URL) and the settings still hold the old one.
|
|
16
|
+
* ConnectFail nothing accepted the connection.
|
|
17
|
+
* TimedOut it accepted but never answered.
|
|
18
|
+
* (HTTP 530/1033 a tunnel with nothing behind it, 502/504 a tunnel whose server is down, 401/403 a wrong token, 404 a
|
|
19
|
+
* wrong path.)
|
|
20
|
+
*
|
|
21
|
+
* Pure: no imports and no services, so it also runs offline under Lune (scripts/test-net-health.luau). Nothing here
|
|
22
|
+
* ever sees or returns a token.
|
|
23
|
+
|
|
24
|
+
]]
|
|
25
|
+
--* The Roblox HttpError name in an error message ("HttpError: NetFail" -> "NetFail"), or undefined.
|
|
26
|
+
local function httpErrorName(message)
|
|
27
|
+
if message == nil then
|
|
28
|
+
return nil
|
|
29
|
+
end
|
|
30
|
+
local name = string.match(message, "HttpError:%s*(%w+)")
|
|
31
|
+
return if type(name) == "string" then name else nil
|
|
32
|
+
end
|
|
33
|
+
local RUN_LOCAL = "run bun run local on the dev PC (it opens a new tunnel and updates these settings)"
|
|
34
|
+
--[[
|
|
35
|
+
*
|
|
36
|
+
* Help for a failed request. `status` is the HTTP status (0: no answer at all) and `message` the error text (from the
|
|
37
|
+
* thrown HttpError, or "HTTP 530" style text for a status; either may be missing).
|
|
38
|
+
|
|
39
|
+
]]
|
|
40
|
+
local describeStatus
|
|
41
|
+
local function describeFailure(status, message)
|
|
42
|
+
if status > 0 then
|
|
43
|
+
return describeStatus(status)
|
|
44
|
+
end
|
|
45
|
+
local _condition = message
|
|
46
|
+
if _condition == nil then
|
|
47
|
+
_condition = ""
|
|
48
|
+
end
|
|
49
|
+
local text = _condition
|
|
50
|
+
local name = httpErrorName(text)
|
|
51
|
+
local lower = string.lower(text)
|
|
52
|
+
if (string.find(lower, "not enabled", 1, true)) ~= nil or (string.find(lower, "httpenabled", 1, true)) ~= nil then
|
|
53
|
+
return {
|
|
54
|
+
reason = "HttpService is off for this place",
|
|
55
|
+
fix = "turn on Game Settings > Security > Allow HTTP Requests (HttpService.HttpEnabled).",
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
if (string.find(lower, "exceeded limit", 1, true)) ~= nil or (string.find(lower, "too many requests", 1, true)) ~= nil then
|
|
59
|
+
return {
|
|
60
|
+
reason = "Roblox's HTTP limit for this server is used up",
|
|
61
|
+
fix = "a server may send 500 requests a minute in all; analytics sends about 10, so something else is using the rest.",
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
if name == "NetFail" then
|
|
65
|
+
return {
|
|
66
|
+
reason = "NetFail: the connection broke mid-request",
|
|
67
|
+
fix = `the dev PC's quick tunnel or server restarted, dropped it or is overloaded (a quick tunnel is for testing; use a real domain in production). Run typetorch doctor; if the address is stale, {RUN_LOCAL}.`,
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
if name == "DnsResolve" then
|
|
71
|
+
return {
|
|
72
|
+
reason = "DnsResolve: the host name doesn't resolve",
|
|
73
|
+
fix = `for a trycloudflare.com address the quick tunnel is gone and the URL changed: {RUN_LOCAL}. Otherwise check the URL for typos (typetorch doctor).`,
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
if name == "ConnectFail" then
|
|
77
|
+
return {
|
|
78
|
+
reason = "ConnectFail: nothing accepted the connection",
|
|
79
|
+
fix = `the server or its tunnel is down, or the address or port is wrong. Run typetorch doctor; {RUN_LOCAL} restarts both.`,
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
if name == "TimedOut" then
|
|
83
|
+
return {
|
|
84
|
+
reason = "TimedOut: the server didn't answer in time",
|
|
85
|
+
fix = "it is overloaded, asleep (the dev PC suspended?) or its tunnel is stuck. Run typetorch doctor.",
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
if name == "SslConnectFail" or name == "SslVerificationFail" then
|
|
89
|
+
return {
|
|
90
|
+
reason = `{name}: the TLS certificate was refused`,
|
|
91
|
+
fix = "use an address with a valid certificate (the Cloudflare tunnel, or Caddy on a VPS).",
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
if name == "InvalidUrl" then
|
|
95
|
+
return {
|
|
96
|
+
reason = "InvalidUrl: the address isn't a valid URL",
|
|
97
|
+
fix = "set it again with typetorch backend setup (typetorch doctor tests it).",
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
if name == "TooManyRedirects" or name == "InvalidRedirect" then
|
|
101
|
+
return {
|
|
102
|
+
reason = `{name}: the address redirects`,
|
|
103
|
+
fix = "use the final address directly in the settings.",
|
|
104
|
+
}
|
|
105
|
+
end
|
|
106
|
+
if name ~= nil then
|
|
107
|
+
return {
|
|
108
|
+
reason = `HttpError {name}`,
|
|
109
|
+
fix = "run typetorch doctor to test the address and token.",
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
return {
|
|
113
|
+
reason = if text == "" then "no answer" else string.sub(text, 1, 120),
|
|
114
|
+
fix = "run typetorch doctor to test the address and token.",
|
|
115
|
+
}
|
|
116
|
+
end
|
|
117
|
+
function describeStatus(status)
|
|
118
|
+
if status == 530 then
|
|
119
|
+
return {
|
|
120
|
+
reason = "HTTP 530: the tunnel has nothing behind it",
|
|
121
|
+
fix = `the dev PC's tunnel or server stopped: {RUN_LOCAL}.`,
|
|
122
|
+
}
|
|
123
|
+
end
|
|
124
|
+
if status == 502 or status == 504 then
|
|
125
|
+
return {
|
|
126
|
+
reason = `HTTP {status}: the server behind the tunnel doesn't answer`,
|
|
127
|
+
fix = `start the analytics server again: {RUN_LOCAL}.`,
|
|
128
|
+
}
|
|
129
|
+
end
|
|
130
|
+
if status == 401 or status == 403 then
|
|
131
|
+
return {
|
|
132
|
+
reason = `HTTP {status}: the server refused the token`,
|
|
133
|
+
fix = "put the server's API key (TYPETORCH_API_KEY; TT_ANALYTICS_INGEST_TOKENS before the rename) in the settings (typetorch backend setup); typetorch doctor tests it.",
|
|
134
|
+
}
|
|
135
|
+
end
|
|
136
|
+
if status == 404 then
|
|
137
|
+
return {
|
|
138
|
+
reason = "HTTP 404: no such address on the server",
|
|
139
|
+
fix = `the URL is wrong (analytics ends in /v1/ingest) or the tunnel points at nothing. Run typetorch doctor; {RUN_LOCAL}.`,
|
|
140
|
+
}
|
|
141
|
+
end
|
|
142
|
+
if status == 408 then
|
|
143
|
+
return {
|
|
144
|
+
reason = "HTTP 408: the server timed out",
|
|
145
|
+
fix = "it is overloaded; the engine retries with a growing pause.",
|
|
146
|
+
}
|
|
147
|
+
end
|
|
148
|
+
if status == 413 then
|
|
149
|
+
return {
|
|
150
|
+
reason = "HTTP 413: the batch was too large",
|
|
151
|
+
fix = "the engine splits it and sends again.",
|
|
152
|
+
}
|
|
153
|
+
end
|
|
154
|
+
if status == 429 then
|
|
155
|
+
return {
|
|
156
|
+
reason = "HTTP 429: rate limited",
|
|
157
|
+
fix = "too many requests for the server or tunnel; the engine retries with a growing pause.",
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
if status >= 500 then
|
|
161
|
+
return {
|
|
162
|
+
reason = `HTTP {status}: the server is failing`,
|
|
163
|
+
fix = "check the analytics server's log.",
|
|
164
|
+
}
|
|
165
|
+
end
|
|
166
|
+
return {
|
|
167
|
+
reason = `HTTP {status}`,
|
|
168
|
+
fix = "run typetorch doctor to test the address and token.",
|
|
169
|
+
}
|
|
170
|
+
end
|
|
171
|
+
--* Help for a kernel Fleet sender error text: "HTTP 530", or an HttpError message (`status().fleet.lastError`).
|
|
172
|
+
local function describeFleetError(lastError)
|
|
173
|
+
if lastError == nil then
|
|
174
|
+
return nil
|
|
175
|
+
end
|
|
176
|
+
local code = string.match(lastError, "^HTTP (%d+)$")
|
|
177
|
+
if type(code) == "string" then
|
|
178
|
+
local _condition = tonumber(code)
|
|
179
|
+
if _condition == nil then
|
|
180
|
+
_condition = 0
|
|
181
|
+
end
|
|
182
|
+
return describeFailure(_condition, lastError)
|
|
183
|
+
end
|
|
184
|
+
return describeFailure(0, lastError)
|
|
185
|
+
end
|
|
186
|
+
return {
|
|
187
|
+
httpErrorName = httpErrorName,
|
|
188
|
+
describeFailure = describeFailure,
|
|
189
|
+
describeFleetError = describeFleetError,
|
|
190
|
+
}
|
package/out/analytics/queue.d.ts
CHANGED
|
@@ -44,6 +44,11 @@ export interface BucketState {
|
|
|
44
44
|
export declare function takeToken(state: BucketState, now: number, burst: number, perMinute: number): boolean;
|
|
45
45
|
/** Seconds to wait after `failures` failed requests in a row: 5, 10, 20... at most 300, spread by `jitter` (0..1). */
|
|
46
46
|
export declare function backoffSeconds(failures: number, jitter: number): number;
|
|
47
|
+
/**
|
|
48
|
+
* Seconds to wait after a settings problem (401/403/404: wrong token or URL): long, because retrying doesn't help until
|
|
49
|
+
* the settings change (that retries at once), spread by `jitter` (0..1) so a whole fleet of servers doesn't retry in step.
|
|
50
|
+
*/
|
|
51
|
+
export declare function configWaitSeconds(jitter: number): number;
|
|
47
52
|
/** What to do with a batch after an HTTP status (0 = no response: network error, timeout, HttpService off). */
|
|
48
53
|
export type Outcome = "sent" | "retry" | "split" | "drop" | "config";
|
|
49
54
|
/**
|
package/out/analytics/queue.luau
CHANGED
|
@@ -192,6 +192,15 @@ local function backoffSeconds(failures, jitter)
|
|
|
192
192
|
local base = math.min(300, 5 * 2 ^ math.min(failures - 1, 10))
|
|
193
193
|
return base * (0.75 + 0.5 * math.clamp(jitter, 0, 1))
|
|
194
194
|
end
|
|
195
|
+
--[[
|
|
196
|
+
*
|
|
197
|
+
* Seconds to wait after a settings problem (401/403/404: wrong token or URL): long, because retrying doesn't help until
|
|
198
|
+
* the settings change (that retries at once), spread by `jitter` (0..1) so a whole fleet of servers doesn't retry in step.
|
|
199
|
+
|
|
200
|
+
]]
|
|
201
|
+
local function configWaitSeconds(jitter)
|
|
202
|
+
return 300 * (0.75 + 0.5 * math.clamp(jitter, 0, 1))
|
|
203
|
+
end
|
|
195
204
|
--* What to do with a batch after an HTTP status (0 = no response: network error, timeout, HttpService off).
|
|
196
205
|
--[[
|
|
197
206
|
*
|
|
@@ -219,6 +228,7 @@ return {
|
|
|
219
228
|
newQueueState = newQueueState,
|
|
220
229
|
takeToken = takeToken,
|
|
221
230
|
backoffSeconds = backoffSeconds,
|
|
231
|
+
configWaitSeconds = configWaitSeconds,
|
|
222
232
|
classifyStatus = classifyStatus,
|
|
223
233
|
RowQueue = RowQueue,
|
|
224
234
|
}
|
|
@@ -5,10 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
/** Row format version (column `v`). */
|
|
7
7
|
export declare const SCHEMA_VERSION = 1;
|
|
8
|
-
/** The ConfigService key (server-only) that holds the sink settings. */
|
|
9
|
-
export declare const SETTINGS_KEY = "TypeTorchAnalytics";
|
|
10
|
-
/** The kernel's fleet API settings `{ url, token }` (server-only ConfigService key); identities can go there. */
|
|
11
|
-
export declare const FLEET_SETTINGS_KEY = "TypeTorchFleet";
|
|
12
8
|
/** The first-session recording codec (column `codec`). */
|
|
13
9
|
export declare const RECORDING_CODEC = "tt-rec-1";
|
|
14
10
|
/** Most bytes of the `props` JSON text. Larger props become `{"_trunc":<bytes>}`. */
|
|
@@ -91,7 +87,7 @@ export interface ExperimentOverride {
|
|
|
91
87
|
/** Everyone gets this variant (must be one of the game's variants). */
|
|
92
88
|
variant?: string;
|
|
93
89
|
}
|
|
94
|
-
/** The sink settings:
|
|
90
|
+
/** The sink settings: the signed settings' `analytics` (kernel 0.3.8, server-only), or `new AnalyticsEngine({ settings })`. */
|
|
95
91
|
export interface AnalyticsSettings {
|
|
96
92
|
backend: "basin" | "duckdb";
|
|
97
93
|
/** basin: the events stream's ingest URL; duckdb: the analytics server's ingest URL. */
|
|
@@ -109,12 +105,12 @@ export interface AnalyticsSettings {
|
|
|
109
105
|
/** Live experiment overrides by experiment name. */
|
|
110
106
|
experiments?: Record<string, ExperimentOverride>;
|
|
111
107
|
/**
|
|
112
|
-
* basin: where identity rows go (the fleet API's `POST /v1/identity` URL). Default: the `
|
|
108
|
+
* basin: where identity rows go (the fleet API's `POST /v1/identity` URL). Default: the settings' `fleet` url
|
|
113
109
|
* + /v1/identity when the server can read it; without either, identities aren't sent. duckdb: unused (they go in
|
|
114
110
|
* the batch body).
|
|
115
111
|
*/
|
|
116
112
|
identity?: string;
|
|
117
|
-
/** basin: the token for `identity` (default: the `
|
|
113
|
+
/** basin: the token for `identity` (default: the settings' `fleet` token). Never logged. */
|
|
118
114
|
identityToken?: string;
|
|
119
115
|
}
|
|
120
116
|
/** AnalyticsSettings with every default filled in. */
|
|
@@ -147,7 +143,7 @@ export interface AnalyticsPurchase {
|
|
|
147
143
|
}
|
|
148
144
|
/** What the automatic collectors do. Everything is on by default. */
|
|
149
145
|
export interface AnalyticsOptions {
|
|
150
|
-
/** Server only: the sink settings. When set, the
|
|
146
|
+
/** Server only: the sink settings. When set, the signed settings aren't read (tests, Studio, kernels before 0.3.8). */
|
|
151
147
|
settings?: AnalyticsSettings;
|
|
152
148
|
/** Server: joins, leaves, device, join source, first-ever vs returning. Default true. */
|
|
153
149
|
sessions?: boolean;
|
|
@@ -197,9 +193,25 @@ export interface AnalyticsStats {
|
|
|
197
193
|
dropped: number;
|
|
198
194
|
/** Rows the backend refused (HTTP 4xx). */
|
|
199
195
|
rejected: number;
|
|
200
|
-
/** Failed requests in a row. */
|
|
196
|
+
/** Failed requests in a row (0 after a success). */
|
|
201
197
|
failures: number;
|
|
198
|
+
/** Failed requests since the server started, every attempt (across swaps). */
|
|
199
|
+
failed: number;
|
|
200
|
+
/**
|
|
201
|
+
* The last failure's message ("both: HttpError: NetFail", "both: HTTP 530 ..."). Kept after a later success:
|
|
202
|
+
* compare `lastErrorAt` with `lastOkAt`.
|
|
203
|
+
*/
|
|
202
204
|
lastError?: string;
|
|
205
|
+
/** HTTP status of the last failure (0: no answer at all, an HttpError). */
|
|
206
|
+
lastStatus?: number;
|
|
207
|
+
/** os.time() of the last failed request and of the last accepted one. */
|
|
208
|
+
lastErrorAt?: number;
|
|
209
|
+
lastOkAt?: number;
|
|
210
|
+
/** Seconds until the next attempt while backing off after failures. */
|
|
211
|
+
retryIn?: number;
|
|
212
|
+
/** While failing: what the last failure means and what to do (hints.ts); the same words the log line and the dev menu use. */
|
|
213
|
+
reason?: string;
|
|
214
|
+
fix?: string;
|
|
203
215
|
/** Settings problems (never contains the token). */
|
|
204
216
|
settingsErrors: string[];
|
|
205
217
|
}
|