@typetorch/framework 0.3.2 → 0.3.5
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 -11
- package/out/analytics/SCHEMA.md +19 -10
- package/out/analytics/engine.d.ts +8 -2
- package/out/analytics/engine.luau +32 -27
- package/out/analytics/protocol.d.ts +4 -1
- package/out/analytics/protocol.luau +6 -3
- package/out/analytics/schema.d.ts +4 -8
- package/out/analytics/schema.luau +1 -7
- package/out/analytics/server.d.ts +15 -1
- package/out/analytics/server.luau +190 -130
- package/out/analytics/settings.d.ts +3 -3
- package/out/analytics/settings.luau +3 -3
- package/out/detached.d.ts +26 -0
- package/out/detached.luau +95 -0
- package/out/devtools/admin-server.d.ts +4 -1
- package/out/devtools/admin-server.luau +31 -53
- package/out/devtools/admin-ui.luau +12 -2
- package/out/devtools/assets-ui.d.ts +10 -0
- package/out/devtools/assets-ui.luau +51 -26
- package/out/devtools/client.luau +555 -221
- package/out/devtools/feeds.d.ts +53 -0
- package/out/devtools/feeds.luau +200 -0
- package/out/devtools/health.d.ts +40 -3
- package/out/devtools/health.luau +381 -26
- package/out/devtools/layout.d.ts +142 -0
- package/out/devtools/layout.luau +766 -0
- package/out/devtools/network-inspector.d.ts +2 -0
- package/out/devtools/network-inspector.luau +302 -167
- package/out/devtools/panes.d.ts +60 -0
- package/out/devtools/panes.luau +1388 -0
- package/out/devtools/roll-call.d.ts +11 -6
- package/out/devtools/roll-call.luau +11 -6
- package/out/devtools/server.luau +22 -1
- package/out/devtools/state-ui.d.ts +5 -0
- package/out/devtools/state-ui.luau +12 -5
- package/out/index.d.ts +8 -1
- package/out/init.luau +6 -0
- package/out/kernel.d.ts +303 -5
- package/out/kernel.luau +39 -2
- 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 +364 -0
- package/out/settings.d.ts +49 -0
- package/out/settings.luau +236 -0
- package/out/typetorch.d.ts +70 -1
- package/out/typetorch.luau +82 -3
- 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.
|
|
@@ -120,6 +154,17 @@ export class CoinService extends Module implements OnStart {
|
|
|
120
154
|
logs (server, own client, other players' clients), client and server dex, network stats, module state, a branch and
|
|
121
155
|
build picker (Server > Branch) and a Claude prompt. Prod-channel servers are read-only. The window can be dragged by
|
|
122
156
|
its header and resized from its corner (double-tap the header to reset).
|
|
157
|
+
- **Panes and windows (dev menu):** every page shows in a pane with a small header: tap the title to pick another
|
|
158
|
+
page, or use Split right, Split down, Open in new window and Close. Drag the line between two panes to resize them.
|
|
159
|
+
The sidebar opens pages in the focused pane (the highlighted one; tap a pane to focus it). Floating windows move by
|
|
160
|
+
their title bar and resize from their corner (double-tap the title bar to maximise); their title bar also has
|
|
161
|
+
Minimise, Dock (back into the main panel) and Close. Windows can be split too. The layout, the windows and each
|
|
162
|
+
pane's own settings (for example the open nodes of two Modules > State panes) survive swaps; a fresh join starts
|
|
163
|
+
with one pane. Claude, Server > Branch and the Manage pages open once (picking them again focuses their pane); every
|
|
164
|
+
other page can be open several times. Panes nobody sees (menu closed, window minimised) stop refreshing, and panes
|
|
165
|
+
on the same data share one request (Status, Overview, Assets, Network > Stats, server Logs, Packets); State panes
|
|
166
|
+
share one query budget. On phones and small screens panes only stack (two at most), and windows open maximised with
|
|
167
|
+
one pane each.
|
|
123
168
|
- **State explorer (Modules > State, Server | Client):** every live module of the running generation (services on the
|
|
124
169
|
server, controllers on the client) and the persist store. Tap a row to open it: the module's own fields (not its
|
|
125
170
|
methods), then tables, arrays, Maps and Sets, 100 entries per page (Prev / Next). Each value has a short preview:
|
|
@@ -171,6 +216,13 @@ this.trove.add(TypeTorch.onUpdatePending((update) => (hint.Visible = !update.can
|
|
|
171
216
|
|
|
172
217
|
// State that survives swaps (plain data only)
|
|
173
218
|
const scores = TypeTorch.persist("scores.v1", () => new Map<number, number>());
|
|
219
|
+
// Per player, removed when the player really leaves (same as this.ctx.playerState)
|
|
220
|
+
const cooldowns = TypeTorch.playerState("cooldowns.v1", () => ({ lastUse: 0 }));
|
|
221
|
+
cooldowns.get(player).lastUse = os.clock();
|
|
222
|
+
|
|
223
|
+
// The running module of a type (same as Dependency<T>()), from onInit/onStart on
|
|
224
|
+
TypeTorch.module<ShopService>().open(player);
|
|
225
|
+
TypeTorch.tryModule<ShopService>()?.open(player); // undefined instead of an error
|
|
174
226
|
|
|
175
227
|
// Dev and roles (server: the kernel decides; client: only the local player, cosmetic)
|
|
176
228
|
if (TypeTorch.isOwner(player)) showOwnerPanel(player);
|
|
@@ -187,6 +239,23 @@ TypeTorch.onLog((entry) => errors.push(entry)); // don't print from inside it
|
|
|
187
239
|
|
|
188
240
|
// Hot assets (below): same as hotAsset(...)
|
|
189
241
|
const shop = TypeTorch.asset("ui/shop");
|
|
242
|
+
|
|
243
|
+
// Live values, server only (kernel 0.3.8): the signed settings record's `game` field (`typetorch settings set game.<key> <json>`).
|
|
244
|
+
const price = TypeTorch.liveConfig("shop.price", { default: 50, parse: (raw) => (typeIs(raw, "number") ? raw : 50) });
|
|
245
|
+
price.get();
|
|
246
|
+
this.trove.add(price.onChanged((value) => this.reprice(value)));
|
|
247
|
+
|
|
248
|
+
// Cross-server, server only. Kernel 0.3.8: game topics over ONE kernel-held MessagingService topic; listeners hear
|
|
249
|
+
// prod servers and this branch by default (dev branches never reach prod); publish queues and retries, max 1 KiB.
|
|
250
|
+
this.trove.add(TypeTorch.messaging.subscribe<Ban>("1guard", (ban, meta) => applyBan(ban, meta.jobId)));
|
|
251
|
+
TypeTorch.messaging.publish("1guard", { kind: "ban", userId }, { to: "prod" });
|
|
252
|
+
|
|
253
|
+
// The universe's live servers (the roll call; cached per server, yields up to ~3 s when stale) and this server's public fields
|
|
254
|
+
TypeTorch.setServerInfo({ region: "eu", vc: true }); // JSON, at most 400 bytes, kept across swaps
|
|
255
|
+
const list = TypeTorch.servers(); // [{ jobId, placeVersion, players, maxPlayers, serverType, branch, channel, uptime, here, info }]
|
|
256
|
+
|
|
257
|
+
// 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.
|
|
258
|
+
TypeTorch.runDetached(() => store.StartSessionAsync(`Player_${userId}`)).then((profile) => attach(player, profile));
|
|
190
259
|
```
|
|
191
260
|
|
|
192
261
|
- **`startInfo.reason`:** `boot`, `deploy`, `rollback`, `branch`, `pin`, `reload`, `server_rollback`,
|
|
@@ -203,6 +272,25 @@ const shop = TypeTorch.asset("ui/shop");
|
|
|
203
272
|
reason and timings, the next artifact in `onSwapOut`, `onUpdatePending`, server-side `onPlayerDevChanged` and
|
|
204
273
|
`requestReload`. On older kernels `startInfo` still knows boot vs swap, the previous artifact and branch (the
|
|
205
274
|
framework records them), `onBranchChanged` still fires, and the reason of a swap is `unknown`.
|
|
275
|
+
- **`messaging`** (kernel 0.3.8): `subscribe(topic, (data, meta) => ...)` returns a disconnect; `meta` = `{ channel,
|
|
276
|
+
branch, jobId, serverType, placeVersion, sentAt, self, replayed? }`; `{ from: "all" | "prod" | "branch" }` changes
|
|
277
|
+
who it hears. `publish(topic, data, { to })` throws on a bad topic, non-JSON data or a message over 1 KiB (with its
|
|
278
|
+
size), returns false when dropped at once (queue full). Studio loops back; the cloud test publishes nothing; older
|
|
279
|
+
kernels throw "needs kernel 0.3.8". `servers()` works on every kernel (0.3.8 holds the roll call topic, so a swap
|
|
280
|
+
doesn't subscribe again).
|
|
281
|
+
- **`liveConfig(key, { default, parse? })`** (kernel 0.3.8, plans/20): reads `game[key]` of the signed settings record the
|
|
282
|
+
kernel verified (signed by both prod keys, so game code can't forge it); `get()` and `onChanged(fn)` (only on real
|
|
283
|
+
changes; generation-scoped). The default without a record or key, when `parse` throws (warned per value), and on older
|
|
284
|
+
kernels (warned once: "needs kernel 0.3.8"; no ConfigService fallback). `TypeTorch.settings()` is the whole copy
|
|
285
|
+
(`seq`, `at`, `game`, `analytics`, `fleet`, ...): server only, it holds tokens.
|
|
286
|
+
- **`runDetached(fn)`** (kernel 0.3.8, server only): runs `fn` on a thread the kernel owns, so a deploy's hard stop
|
|
287
|
+
(which kills this generation's threads mid-call) can't cut it off: for ProfileStore / ProfileService loads, saves and
|
|
288
|
+
releases. Returns a Promise that settles while this generation runs; after a swap the result is dropped, so a job
|
|
289
|
+
whose result must survive writes it into `persist` itself. The job keeps this generation's closures alive until it
|
|
290
|
+
ends. Errors reject it and go to the kernel's log with the generation's name (never the health window); at most 256
|
|
291
|
+
run at once per server (then it throws); one past 60 s is logged and flagged in Server > Status. Older kernels
|
|
292
|
+
throw "needs kernel 0.3.8; use the DataHost job queue (Player data guide)": the other option, which works on every
|
|
293
|
+
kernel. `features.runDetached` tells which one this server has.
|
|
206
294
|
- **Edit mode** (UI Labs stories, no kernel): `running` is false, identity has defaults, `persist` keeps a local table,
|
|
207
295
|
events never fire, and the server-only reads throw.
|
|
208
296
|
|
|
@@ -216,6 +304,8 @@ pieces that keep it safe across swaps:
|
|
|
216
304
|
- **Session handles live in `persist`**, keyed by `UserId`. `onPlayerAdded` replays everyone after a swap, so it
|
|
217
305
|
re-attaches to the open session instead of loading again.
|
|
218
306
|
- **Release only on `Players.PlayerRemoving`**, never in `onStop` or `onSwapOut`.
|
|
307
|
+
- **Library calls can't be cut off by a swap:** run them as DataHost jobs (any kernel) or with
|
|
308
|
+
`TypeTorch.runDetached` (kernel 0.3.8).
|
|
219
309
|
- **Split store names by channel** (`TypeTorch.channel === "prod" ? "PlayerData" : "PlayerData_dev"`).
|
|
220
310
|
|
|
221
311
|
Full example with ProfileStore: [Player data guide](https://github.com/typetorch/docs/blob/main/guides/player-data.md).
|
|
@@ -276,7 +366,7 @@ self-hosted DuckDB analytics server). Nothing runs until an engine is created: n
|
|
|
276
366
|
```ts
|
|
277
367
|
import { AnalyticsEngine } from "@typetorch/framework";
|
|
278
368
|
|
|
279
|
-
// A server module (onInit):
|
|
369
|
+
// A server module (onInit): the sink comes from the signed settings' `analytics` (kernel 0.3.8).
|
|
280
370
|
const analytics = new AnalyticsEngine();
|
|
281
371
|
analytics.track(player, "quest_done", { quest: "tutorial" }); // custom
|
|
282
372
|
analytics.step(player, "onboarding", 3, "opened_shop"); // funnels
|
|
@@ -297,20 +387,22 @@ const variant = analytics.experiment("onboarding", ["short", "long"]); // same a
|
|
|
297
387
|
and the next generation sends them. In edit mode (UI Labs) it is inert.
|
|
298
388
|
- **Server calls take the player first** (`track(player, ...)`); without one an event is server-only (no player id).
|
|
299
389
|
Client calls are always about the local player and are marked `src = "client"` (a client can lie). The server
|
|
300
|
-
checks their shape, size (props at most 4 KB) and rate.
|
|
390
|
+
checks their shape, size (props at most 4 KB) and rate. `purchase` and `currency` are server-only (revenue and the
|
|
391
|
+
economy are server-authoritative): on the client they warn once and send nothing, and the server refuses them from
|
|
392
|
+
clients.
|
|
301
393
|
- **Options** (all on by default): `sessions` (joins, leaves, device, join source, account age bucket, Premium,
|
|
302
394
|
country, friends in the server, first-ever vs returning, days since the last visit), `tech` (FPS, ping, memory, load
|
|
303
395
|
time, client and server errors, swaps and rollbacks, leaves within 60 s of a swap), `zones` (parts or models tagged
|
|
304
396
|
`TTZone`, named by a `Name` attribute or the instance name), `screens` (ScreenGuis in PlayerGui, GuiObjects tagged
|
|
305
397
|
`TTScreen`), `recording` (the first-ever session in detail), `fleet` (kernel 0.3.2 heartbeats and deploy reports),
|
|
306
398
|
`identity` (one `{ pid, uid }` row per session to the dev's own server, below). `settings` (server only) replaces the
|
|
307
|
-
|
|
399
|
+
signed settings' `analytics`, for tests, Studio and kernels before 0.3.8.
|
|
308
400
|
- **Every row** carries the time (server clock), a random player id (never the UserId), the session, the server
|
|
309
401
|
(JobId, type, place), the artifact (id, seq, branch, channel), the device, new vs returning, the player's state
|
|
310
402
|
(`zone:Lobby|screen:Shop|activity:round`) and experiment variants. The exact rows: `src/analytics/SCHEMA.md`.
|
|
311
403
|
- **Experiments:** `experiment(...)` is deterministic per player and name (they keep their variant in every session),
|
|
312
404
|
may yield briefly the first time (until the player's id loads), and stamps the variant on the player's later events.
|
|
313
|
-
The settings
|
|
405
|
+
The settings turn one off (`active: false`: everyone gets the first variant), sets `weights` or forces a
|
|
314
406
|
`variant`, live.
|
|
315
407
|
- **First-ever session:** for new players (a share of them, `recordShare`), the client records character and camera
|
|
316
408
|
about 10 times a second, every input (never while a TextBox or the chat has focus; text boxes only say which box was
|
|
@@ -320,7 +412,7 @@ const variant = analytics.experiment("onboarding", ["short", "long"]); // same a
|
|
|
320
412
|
row carries a UserId.
|
|
321
413
|
- **Identities** (option `identity`, default on): once the pid is known, one row `{ pid, uid, t }` (the UserId, nothing
|
|
322
414
|
else) per session goes to the dev's own server only, never into the events: duckdb games in the batch body, Basin
|
|
323
|
-
games to the fleet API (`
|
|
415
|
+
games to the fleet API (the settings' `fleet` url + `/v1/identity`, or the `identity` / `identityToken` settings). The
|
|
324
416
|
analytics server keeps pid -> UserId in a deletable table (support lookups, Right to Erasure).
|
|
325
417
|
- **Player ids:** DataStore `TypeTorchAnalytics`, key `p/<UserId>` -> `{ pid, first, last }`: one read per join, a
|
|
326
418
|
write on the first join and at leave. Deleting the key (Right to Erasure) leaves that player's rows anonymous.
|
|
@@ -328,10 +420,21 @@ const variant = analytics.experiment("onboarding", ["short", "long"]); // same a
|
|
|
328
420
|
`flushSeconds` (15) or at 500 rows, at most ~10 HttpService requests a minute, retries with backoff, and a last
|
|
329
421
|
flush on shutdown (kernel 0.3.2). Delivery is at least once (a swap mid-request sends that batch again).
|
|
330
422
|
`analytics.stats()` (server) has the counters; `flush()` sends soon.
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
423
|
+
- **The cloud test sends nothing.** `typetorch test --cloud` boots the payload headless in a Luau Execution task where
|
|
424
|
+
HttpService works; there the engine (stub kernel `test = true`, or `workspace:GetAttribute("TypeTorchTest")`) collects
|
|
425
|
+
as usual but never uploads: no event rows, no identity rows, no HTTP request, so a prod deploy never puts a fake
|
|
426
|
+
server session into your analytics.
|
|
427
|
+
- **Bounds on what a client can cause:** at most 32 experiments per session (each is stamped on every later row), and
|
|
428
|
+
a client's `exp` messages count against its event budget (120 a minute, 5,000 a session). Clients can't send
|
|
429
|
+
`purchase` or `currency` rows; the analytics server's ingest also refuses client-marked ones (older engines) and its
|
|
430
|
+
revenue queries count `purchase` rows the server sent.
|
|
431
|
+
- **Friends in the server** (the join's `friends`): one `Players:GetFriendsAsync` per join (at most 10 pages, none
|
|
432
|
+
when the player is alone) intersected with the players there, not a web call per player.
|
|
433
|
+
|
|
434
|
+
**Settings** (server only, never sent to clients): the signed settings record's `analytics` field (kernel 0.3.8,
|
|
435
|
+
plans/20), written with `typetorch settings set analytics -` (JSON on stdin; `@typetorch/analytics`' `writeSettings()`
|
|
436
|
+
and `bun run local` call it). Every new copy applies live within seconds (the CLI pings servers). Before kernel 0.3.8
|
|
437
|
+
there are no settings unless the game passes `new AnalyticsEngine({ settings })`:
|
|
335
438
|
|
|
336
439
|
```json
|
|
337
440
|
{ "backend": "basin", "events": "https://<stream-id>.ingest.cloudflare.com",
|
|
@@ -367,6 +470,16 @@ bun run build # rbxtsc --type package -> out/
|
|
|
367
470
|
`scripts/test-state-inspect.luau` checks the state explorer's core (previews, raw reads only, cycles, paging,
|
|
368
471
|
Maps/Sets with Instance keys, the depth limit, filters, request validation) and that `src/version.ts` matches
|
|
369
472
|
package.json.
|
|
473
|
+
`scripts/test-layout.luau` checks the dev menu's pane layout core (`devtools/layout.ts`: splits and their limits,
|
|
474
|
+
closing, pop out and dock, singleton pages, ratios, focus, and cleaning a remembered layout).
|
|
475
|
+
`scripts/test-modules.luau` runs the real `startServer`/`startClient` of `out/` (with RuntimeLib, Promise, trove and
|
|
476
|
+
t from node_modules) over several generations that share a persist store: `Dependency<T>()`, `TypeTorch.module` /
|
|
477
|
+
`tryModule` and their errors, `Lazy<T>` (parameters and fields, cycles, start order), network `emit` and request
|
|
478
|
+
timeouts and `playerState` (swaps, leaves).
|
|
479
|
+
`scripts/test-flamework-compat.luau` (same harness) drives `createFlameworkCompat` through each generation's
|
|
480
|
+
dispatcher: connect (one thread per call, guards, Disconnect, troves), fire / except / broadcast / the call
|
|
481
|
+
shorthand, setCallback with values and Promises, predict, a swap, and the client side (invoke, invokeWithTimeout,
|
|
482
|
+
`createClient({ defaultTimeout })`).
|
|
370
483
|
`scripts/test-analytics.luau` checks the analytics engine's pure parts (experiment assignment, settings, the queue
|
|
371
484
|
and HTTP budget, tt-rec-1, the sink request bodies); `test-analytics-server.luau` and `test-analytics-client.luau`
|
|
372
485
|
run the compiled server and client cores against mocked services (sessions, intake, retries, a swap with a request
|
package/out/analytics/SCHEMA.md
CHANGED
|
@@ -31,7 +31,7 @@ Integers are written with every digit (never exponent notation).
|
|
|
31
31
|
| `state` | string | The player's state **after** this event: `zone:<z>\|screen:<s>\|activity:<a>`, empty parts left out (`""` when nothing is known). Server-only events: `activity:<server activity>` or `""` |
|
|
32
32
|
| `exp` | string | JSON object of the player's active experiments, `{"onboarding":"short"}`; `{}` when none |
|
|
33
33
|
| `sexp` | string | The server's experiment: the artifact id of a kernel A/B experiment pin, or `""` |
|
|
34
|
-
| `src` | string | `server`, or `client` (sent by a client: it can lie; the server checked shapes and rate) |
|
|
34
|
+
| `src` | string | `server`, or `client` (sent by a client: it can lie; the server checked shapes and rate). Never `client` on `purchase` or `currency` rows |
|
|
35
35
|
| `props` | string | JSON object text, at most 4096 bytes. Too large: `{"_trunc":<bytes>}`; not encodable: `{"_err":"encode"}`; a non-object value `x`: `{"value":x}` |
|
|
36
36
|
|
|
37
37
|
### recordings
|
|
@@ -57,7 +57,7 @@ First-ever-session detail, one row per packed chunk.
|
|
|
57
57
|
|
|
58
58
|
| kind | name | props |
|
|
59
59
|
|---|---|---|
|
|
60
|
-
| session | `join` | `from`: `direct` (Home, search, sorts: Roblox doesn't say), `teleport` (same game), `teleport_game` (another game), `referral` (invite/referral link), `share` (launch data), `follow` (followed someone); `ctx?` (GameJoinContext.JoinSource name); `tp?` (had teleport data); `party?` (players teleported along); `age` (account age: `<1d`, `1-7d`, `7-30d`, `30-90d`, `90-365d`, `1-3y`, `3y+`); `prem` (Premium); `country?` (ISO code); `friends` (friends in the server, at most
|
|
60
|
+
| session | `join` | `from`: `direct` (Home, search, sorts: Roblox doesn't say), `teleport` (same game), `teleport_game` (another game), `referral` (invite/referral link), `share` (launch data), `follow` (followed someone); `ctx?` (GameJoinContext.JoinSource name); `tp?` (had teleport data); `party?` (players teleported along); `age` (account age: `<1d`, `1-7d`, `7-30d`, `30-90d`, `90-365d`, `1-3y`, `3y+`); `prem` (Premium); `country?` (ISO code); `friends` (how many of the player's friends are in the server: one friend-list lookup per join, at most 10 pages, `0` when it fails); `ret` (days since the last visit, `-1` first visit or unknown); `late?` (the engine started over 60 s after the join). `t` is the join time |
|
|
61
61
|
| session | `device` (src client) | `input` (`kbm`, `touch`, `gamepad`, `vr`, `unknown`), `w`, `h` (viewport points), `touch`, `kb`, `mouse`, `pad`, `vr`. Once per session |
|
|
62
62
|
| session | `leave` | `secs` (session length), `why`: `left`, `shutdown`, `gone` (left while no generation ran the engine), `swap60?` (left within 60 s of a hot swap) |
|
|
63
63
|
| tech | `server` | Every `techEvery` s: `fps` (physics), `hb` (Heartbeat rate), `mem` (MB, 10s), `players`, `ping50?`, `ping90?` (ms), `q`, `qr` (queued events, recordings), `dropped`, `sent`, `fails`, `health?`, `errors?` (kernel 0.3.2 health) |
|
|
@@ -71,17 +71,21 @@ First-ever-session detail, one row per packed chunk.
|
|
|
71
71
|
| zone | `leave` | `zone`, `to?`, `secs` |
|
|
72
72
|
| state | `activity` | `to`, `from` (server or client src) |
|
|
73
73
|
| state | `screen` | `to`, `from` (`""` = none). Client: ScreenGuis in PlayerGui (Enabled), GuiObjects tagged `TTScreen` (Visible), `screen()` |
|
|
74
|
-
| experiment | `<experiment>` | `variant`, `variants` (count), `forced?`. Once per session when assigned or changed |
|
|
74
|
+
| experiment | `<experiment>` | `variant`, `variants` (count), `forced?`. Once per session when assigned or changed. At most 32 experiments per session: a 33rd `experiment()` returns the control (the first variant) and warns. A client's experiment calls count against its event budget (below) |
|
|
75
75
|
| recording_meta | `start` | `share` (the recorded share then) |
|
|
76
76
|
| recording_meta | `end` | `chunks`, `bytes`, `why`: `window` (60 s after the first input), `cap` (10 min, or the server's 256 KB / 80 chunks), `left`, `end` |
|
|
77
77
|
| fleet | `heartbeat` | The kernel's `fleetStatus()` (kernel 0.3.2+), every 60 ± 10 s and once at each generation start. pid `""` |
|
|
78
78
|
| fleet | `deploy_report` | One kernel deploy report `{s, b, a, j, r, e?, d?, t, g, k, p}` (r: `swapped`, `failed`, `rolled_back`, `skipped`, `booted`), once per (s, j, r) per server, across generations. pid `""` |
|
|
79
79
|
| custom | `<name>` | `track(name, props)`: the game's props |
|
|
80
80
|
| funnel | `<funnel>` | `step(funnel, index, name?)`: `i`, `step?` |
|
|
81
|
-
| purchase | `<kind>` | `purchase({product, robux, where?, kind?})`: name = `kind` (default `product`); `product`, `robux`, `where?` |
|
|
82
|
-
| currency | `<currency>` | `currency(name, delta, reason)`: `delta`, `reason` |
|
|
81
|
+
| purchase | `<kind>` | Server only. `purchase(player?, {product, robux, where?, kind?})`: name = `kind` (default `product`); `product`, `robux`, `where?` |
|
|
82
|
+
| currency | `<currency>` | Server only. `currency(player?, name, delta, reason)`: `delta`, `reason` |
|
|
83
83
|
|
|
84
|
-
Client-sent kinds are limited to `custom`, `funnel`, `
|
|
84
|
+
Client-sent kinds are limited to `custom`, `funnel`, `state`, `tech`, within a budget of 120 a minute and 5,000 a
|
|
85
|
+
session per player. `purchase` and `currency` are server-only (revenue and the economy are server-authoritative): on
|
|
86
|
+
the client `purchase()` and `currency()` warn once and send nothing, the server refuses those kinds from clients, and
|
|
87
|
+
the analytics server's ingest refuses such rows marked `src = client` (older engines let clients send them). Revenue
|
|
88
|
+
counts only server-sent `purchase` rows; rows from before `src` existed (no `src`) still count.
|
|
85
89
|
|
|
86
90
|
## tt-rec-1
|
|
87
91
|
|
|
@@ -124,8 +128,10 @@ start, i.e. the row's `t`). Time is the running sum of `dt`.
|
|
|
124
128
|
|
|
125
129
|
## Sink settings
|
|
126
130
|
|
|
127
|
-
|
|
128
|
-
`
|
|
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).
|
|
129
135
|
|
|
130
136
|
```json
|
|
131
137
|
{
|
|
@@ -150,8 +156,8 @@ ConfigService key `TypeTorchAnalytics` (server-only), a JSON object (or a JSON s
|
|
|
150
156
|
| `recordShare` | 1 | 0..1, share of new players recorded (stable per pid) |
|
|
151
157
|
| `techEvery` | 60 | 15..3600 seconds between tech samples |
|
|
152
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) |
|
|
153
|
-
| `identity` | `
|
|
154
|
-
| `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 |
|
|
155
161
|
|
|
156
162
|
Unknown fields are ignored. Settings errors are warned once (never with the token).
|
|
157
163
|
|
|
@@ -174,6 +180,9 @@ Unknown fields are ignored. Settings errors are warned once (never with the toke
|
|
|
174
180
|
other 4xx drop the batch (counted as rejected).
|
|
175
181
|
- **Delivery is at least once:** a hot swap during a request sends its rows again. Readers may drop exact duplicate
|
|
176
182
|
rows.
|
|
183
|
+
- **The cloud test sends nothing:** inside `typetorch test --cloud` (the stub kernel's `test = true`, or the workspace
|
|
184
|
+
attribute `TypeTorchTest`) rows are collected as usual but dropped at each flush: no event, recording or identity
|
|
185
|
+
rows and no HTTP request, so a deploy never puts a fake server session into the analytics.
|
|
177
186
|
- **Queue:** 10,000 events (1,000 while no settings are known), 2,000 fleet rows, 300 recording chunks; over a cap the
|
|
178
187
|
oldest are dropped and counted (`tech/server` `dropped`). The queue lives in the kernel's persist store across hot
|
|
179
188
|
swaps and is flushed on shutdown (kernel 0.3.2 onClose; older kernels: one BindToClose relay).
|
|
@@ -15,10 +15,16 @@ export declare class AnalyticsEngine {
|
|
|
15
15
|
/** A funnel step, e.g. `step("onboarding", 3, "opened_shop")`. Indexes start at 1 by convention. */
|
|
16
16
|
step(funnel: string, index: number, name?: string): void;
|
|
17
17
|
step(player: Player, funnel: string, index: number, name?: string): void;
|
|
18
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* A Robux purchase (log it once the receipt is granted). The event name is `kind` ("product" by default). Server
|
|
20
|
+
* only: revenue is server-authoritative, so on the client this warns once and sends nothing.
|
|
21
|
+
*/
|
|
19
22
|
purchase(purchase: AnalyticsPurchase): void;
|
|
20
23
|
purchase(player: Player, purchase: AnalyticsPurchase): void;
|
|
21
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Currency in (+) or out (-), e.g. `currency(player, "coins", 50, "round_reward")`. Server only: the economy is
|
|
26
|
+
* server-authoritative, so on the client this warns once and sends nothing.
|
|
27
|
+
*/
|
|
22
28
|
currency(name: string, delta: number, reason: string): void;
|
|
23
29
|
currency(player: Player, name: string, delta: number, reason: string): void;
|
|
24
30
|
/**
|
|
@@ -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
|
|
|
@@ -26,6 +26,17 @@ local function isPlayer(value)
|
|
|
26
26
|
end
|
|
27
27
|
return _condition
|
|
28
28
|
end
|
|
29
|
+
--* Server-only calls made on the client: warned once each per generation; the server refuses these kinds from clients.
|
|
30
|
+
local warnedServerOnly = {}
|
|
31
|
+
local function serverOnly(call)
|
|
32
|
+
local _call = call
|
|
33
|
+
if warnedServerOnly[_call] ~= nil then
|
|
34
|
+
return nil
|
|
35
|
+
end
|
|
36
|
+
local _call_1 = call
|
|
37
|
+
warnedServerOnly[_call_1] = true
|
|
38
|
+
warn("[src/analytics/engine.ts:31]", `[analytics] {call}() is server-only (revenue and currency are server-authoritative): the client sends nothing`)
|
|
39
|
+
end
|
|
29
40
|
local function currentServer(options)
|
|
30
41
|
if serverCore then
|
|
31
42
|
return serverCore
|
|
@@ -87,11 +98,11 @@ do
|
|
|
87
98
|
local existing = serverCore ~= nil
|
|
88
99
|
self.server = currentServer(options)
|
|
89
100
|
if existing and options.settings ~= nil then
|
|
90
|
-
warn("[src/analytics/engine.ts:
|
|
101
|
+
warn("[src/analytics/engine.ts:75]", "[analytics] an engine already runs: its settings stay")
|
|
91
102
|
end
|
|
92
103
|
else
|
|
93
104
|
if options.settings ~= nil then
|
|
94
|
-
warn("[src/analytics/engine.ts:
|
|
105
|
+
warn("[src/analytics/engine.ts:77]", "[analytics] settings are read on the server only; the client ignores them")
|
|
95
106
|
end
|
|
96
107
|
self.client = currentClient(options)
|
|
97
108
|
end
|
|
@@ -150,6 +161,10 @@ do
|
|
|
150
161
|
end
|
|
151
162
|
end
|
|
152
163
|
function AnalyticsEngine:purchase(first, second)
|
|
164
|
+
if self.client then
|
|
165
|
+
serverOnly("purchase")
|
|
166
|
+
return nil
|
|
167
|
+
end
|
|
153
168
|
local player = if isPlayer(first) then first else nil
|
|
154
169
|
local info = (if player then second else first)
|
|
155
170
|
local _condition = not info
|
|
@@ -162,7 +177,7 @@ do
|
|
|
162
177
|
end
|
|
163
178
|
end
|
|
164
179
|
if _condition then
|
|
165
|
-
warn("[src/analytics/engine.ts:
|
|
180
|
+
warn("[src/analytics/engine.ts:129]", "[analytics] purchase({ product, robux }) needs numbers")
|
|
166
181
|
return nil
|
|
167
182
|
end
|
|
168
183
|
local _kind = info.kind
|
|
@@ -180,37 +195,27 @@ do
|
|
|
180
195
|
if _result ~= nil then
|
|
181
196
|
_result:track(player, "purchase", kind, props)
|
|
182
197
|
end
|
|
183
|
-
local _result_1 = self.client
|
|
184
|
-
if _result_1 ~= nil then
|
|
185
|
-
_result_1:track("purchase", kind, props)
|
|
186
|
-
end
|
|
187
198
|
end
|
|
188
199
|
function AnalyticsEngine:currency(first, second, third, fourth)
|
|
200
|
+
if self.client then
|
|
201
|
+
serverOnly("currency")
|
|
202
|
+
return nil
|
|
203
|
+
end
|
|
189
204
|
if isPlayer(first) then
|
|
190
|
-
local props = {
|
|
191
|
-
delta = third,
|
|
192
|
-
reason = fourth,
|
|
193
|
-
}
|
|
194
205
|
local _result = self.server
|
|
195
206
|
if _result ~= nil then
|
|
196
|
-
_result:track(first, "currency", second,
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
_result_1:track("currency", second, props)
|
|
207
|
+
_result:track(first, "currency", second, {
|
|
208
|
+
delta = third,
|
|
209
|
+
reason = fourth,
|
|
210
|
+
})
|
|
201
211
|
end
|
|
202
212
|
else
|
|
203
|
-
local props = {
|
|
204
|
-
delta = second,
|
|
205
|
-
reason = third,
|
|
206
|
-
}
|
|
207
213
|
local _result = self.server
|
|
208
214
|
if _result ~= nil then
|
|
209
|
-
_result:track(nil, "currency", first,
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
_result_1:track("currency", first, props)
|
|
215
|
+
_result:track(nil, "currency", first, {
|
|
216
|
+
delta = second,
|
|
217
|
+
reason = third,
|
|
218
|
+
})
|
|
214
219
|
end
|
|
215
220
|
end
|
|
216
221
|
end
|
|
@@ -281,7 +286,7 @@ do
|
|
|
281
286
|
return self.client:experiment(first, variants)
|
|
282
287
|
end
|
|
283
288
|
if self.server then
|
|
284
|
-
warn("[src/analytics/engine.ts:
|
|
289
|
+
warn("[src/analytics/engine.ts:204]", `[analytics] experiment("{first}") on the server needs the player: experiment(player, name, variants)`)
|
|
285
290
|
end
|
|
286
291
|
local _condition = variants[1]
|
|
287
292
|
if _condition == nil then
|
|
@@ -52,7 +52,10 @@ export interface ServerHello {
|
|
|
52
52
|
/** Live experiment overrides by name. */
|
|
53
53
|
exps: Record<string, ExperimentOverride>;
|
|
54
54
|
}
|
|
55
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Kinds a client may send in "ev". Never `purchase` or `currency`: revenue and the economy are server-authoritative
|
|
57
|
+
* (a client could otherwise add fake payers and Robux), so the server refuses them from clients.
|
|
58
|
+
*/
|
|
56
59
|
export declare const CLIENT_KINDS: Set<string>;
|
|
57
60
|
/** A client's hello, checked field by field (it comes from an untrusted client). */
|
|
58
61
|
export declare function cleanHello(value: unknown): HelloInfo | undefined;
|
|
@@ -38,12 +38,15 @@ local INPUTS = {
|
|
|
38
38
|
["vr"] = true,
|
|
39
39
|
["unknown"] = true,
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
--[[
|
|
42
|
+
*
|
|
43
|
+
* Kinds a client may send in "ev". Never `purchase` or `currency`: revenue and the economy are server-authoritative
|
|
44
|
+
* (a client could otherwise add fake payers and Robux), so the server refuses them from clients.
|
|
45
|
+
|
|
46
|
+
]]
|
|
42
47
|
local CLIENT_KINDS = {
|
|
43
48
|
["custom"] = true,
|
|
44
49
|
["funnel"] = true,
|
|
45
|
-
["purchase"] = true,
|
|
46
|
-
["currency"] = true,
|
|
47
50
|
["state"] = true,
|
|
48
51
|
["tech"] = true,
|
|
49
52
|
}
|
|
@@ -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;
|
|
@@ -8,10 +8,6 @@
|
|
|
8
8
|
]]
|
|
9
9
|
--* Row format version (column `v`).
|
|
10
10
|
local SCHEMA_VERSION = 1
|
|
11
|
-
--* The ConfigService key (server-only) that holds the sink settings.
|
|
12
|
-
local SETTINGS_KEY = "TypeTorchAnalytics"
|
|
13
|
-
--* The kernel's fleet API settings `{ url, token }` (server-only ConfigService key); identities can go there.
|
|
14
|
-
local FLEET_SETTINGS_KEY = "TypeTorchFleet"
|
|
15
11
|
--* The first-session recording codec (column `codec`).
|
|
16
12
|
local RECORDING_CODEC = "tt-rec-1"
|
|
17
13
|
--* Most bytes of the `props` JSON text. Larger props become `{"_trunc":<bytes>}`.
|
|
@@ -26,7 +22,7 @@ local PROPS_MAX_BYTES = 4096
|
|
|
26
22
|
|
|
27
23
|
]]
|
|
28
24
|
--* Live overrides of one per-player experiment (settings key `experiments`).
|
|
29
|
-
--* The sink settings:
|
|
25
|
+
--* The sink settings: the signed settings' `analytics` (kernel 0.3.8, server-only), or `new AnalyticsEngine({ settings })`.
|
|
30
26
|
--* AnalyticsSettings with every default filled in.
|
|
31
27
|
--* Event properties: a flat JSON object (nested tables work but are harder to query).
|
|
32
28
|
--* What the automatic collectors do. Everything is on by default.
|
|
@@ -35,8 +31,6 @@ local PROPS_MAX_BYTES = 4096
|
|
|
35
31
|
--* Counters of the server engine (`AnalyticsEngine.stats()`).
|
|
36
32
|
return {
|
|
37
33
|
SCHEMA_VERSION = SCHEMA_VERSION,
|
|
38
|
-
SETTINGS_KEY = SETTINGS_KEY,
|
|
39
|
-
FLEET_SETTINGS_KEY = FLEET_SETTINGS_KEY,
|
|
40
34
|
RECORDING_CODEC = RECORDING_CODEC,
|
|
41
35
|
PROPS_MAX_BYTES = PROPS_MAX_BYTES,
|
|
42
36
|
}
|
|
@@ -63,7 +63,7 @@ export declare class ServerAnalytics {
|
|
|
63
63
|
private settingsErrors;
|
|
64
64
|
/** The first settings read finished (found or not), so experiment overrides are as known as they get. */
|
|
65
65
|
private settingsTried;
|
|
66
|
-
/** The `
|
|
66
|
+
/** The settings' `fleet` value (Basin games send identities to the fleet API). */
|
|
67
67
|
private fleetSettings?;
|
|
68
68
|
private nextFlushAt;
|
|
69
69
|
private backoffUntil;
|
|
@@ -74,6 +74,12 @@ export declare class ServerAnalytics {
|
|
|
74
74
|
private readonly identity;
|
|
75
75
|
private sexp;
|
|
76
76
|
private stopped;
|
|
77
|
+
/**
|
|
78
|
+
* `typetorch test --cloud`: the payload boots headless in a Luau Execution task, where HttpService works. The engine
|
|
79
|
+
* then collects as usual but sends nothing (no rows, no identity rows, no HTTP), or every prod deploy would put a
|
|
80
|
+
* fake server session into the owner's analytics. Set by the stub kernel's `test` or the TypeTorchTest attribute.
|
|
81
|
+
*/
|
|
82
|
+
private readonly testMode;
|
|
77
83
|
constructor(options: AnalyticsOptions, scope: GenerationScope, trove: Trove);
|
|
78
84
|
private serverExperiment;
|
|
79
85
|
private stateOf;
|
|
@@ -109,6 +115,12 @@ export declare class ServerAnalytics {
|
|
|
109
115
|
*/
|
|
110
116
|
private resolve;
|
|
111
117
|
private joinSource;
|
|
118
|
+
/**
|
|
119
|
+
* How many of the player's friends are in this server: one `GetFriendsAsync` per join (at most FRIEND_PAGES_MAX
|
|
120
|
+
* pages) intersected with the players here, instead of an IsFriendsWith call per player. Stops early once every
|
|
121
|
+
* other player was found; after each yield it stops with the count so far if the player left or the generation
|
|
122
|
+
* stopped. No call when the player is alone; a failed call counts what it read (0 at first).
|
|
123
|
+
*/
|
|
112
124
|
private friendsHere;
|
|
113
125
|
private emitJoin;
|
|
114
126
|
private onPlayer;
|
|
@@ -117,6 +129,8 @@ export declare class ServerAnalytics {
|
|
|
117
129
|
private recordingAllowed;
|
|
118
130
|
private sendHello;
|
|
119
131
|
private onHello;
|
|
132
|
+
/** One client-sent event (or an "exp" message, which may add a row): within the player's budget and the session cap. */
|
|
133
|
+
private takeClientEvent;
|
|
120
134
|
private onEvents;
|
|
121
135
|
private onChunk;
|
|
122
136
|
private startIntake;
|