@typetorch/framework 0.3.5 → 0.5.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 +45 -10
- package/out/analytics/SCHEMA.md +9 -4
- package/out/analytics/client.luau +26 -15
- package/out/analytics/hints.d.ts +36 -0
- package/out/analytics/hints.luau +190 -0
- package/out/analytics/protocol.d.ts +21 -0
- package/out/analytics/protocol.luau +28 -0
- package/out/analytics/queue.d.ts +5 -0
- package/out/analytics/queue.luau +10 -0
- package/out/analytics/schema.d.ts +17 -1
- package/out/analytics/server.d.ts +14 -0
- package/out/analytics/server.luau +156 -39
- package/out/analytics/settings.d.ts +8 -1
- package/out/analytics/settings.luau +59 -18
- 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/devtools/access.d.ts +71 -0
- package/out/devtools/access.luau +140 -0
- package/out/devtools/admin-server.d.ts +2 -1
- package/out/devtools/admin-server.luau +20 -15
- 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-ui.luau +52 -5
- package/out/devtools/claude.d.ts +6 -1
- package/out/devtools/claude.luau +118 -52
- package/out/devtools/client.luau +221 -96
- package/out/devtools/explorer-server.d.ts +4 -2
- package/out/devtools/explorer-server.luau +9 -4
- package/out/devtools/health.d.ts +51 -3
- package/out/devtools/health.luau +353 -28
- package/out/devtools/network-inspector.d.ts +7 -0
- package/out/devtools/network-inspector.luau +12 -14
- package/out/devtools/protocol.d.ts +5 -0
- package/out/devtools/remote-debug.d.ts +38 -0
- package/out/devtools/remote-debug.luau +250 -0
- package/out/devtools/server.d.ts +2 -2
- package/out/devtools/server.luau +46 -10
- package/out/devtools/toolbox-server.d.ts +8 -6
- package/out/devtools/toolbox-server.luau +16 -14
- package/out/devtools/widgets.d.ts +8 -0
- package/out/devtools/widgets.luau +14 -1
- package/out/kernel.d.ts +265 -4
- package/out/kernel.luau +65 -0
- package/out/servers.luau +5 -2
- package/out/typetorch.d.ts +13 -1
- package/out/typetorch.luau +12 -4
- package/out/version.d.ts +1 -1
- package/out/version.luau +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -152,8 +152,18 @@ export class CoinService extends Module implements OnStart {
|
|
|
152
152
|
- **Dev menu:** devs (Studio, project members, dev badge) get a DEV button, `Ctrl+Shift+D` and `/tt dev`: artifact
|
|
153
153
|
(with how long the server and client generations have run, the server's uptime and the build's age), server status,
|
|
154
154
|
logs (server, own client, other players' clients), client and server dex, network stats, module state, a branch and
|
|
155
|
-
build picker (Server > Branch) and a Claude prompt.
|
|
156
|
-
its header and resized from its corner (double-tap the
|
|
155
|
+
build picker (Server > Branch) and a Claude prompt. The sidebar, top to bottom: Artifact, Server, Network, Modules,
|
|
156
|
+
Manage, Logs, Dex, Claude. The window can be dragged by its header and resized from its corner (double-tap the
|
|
157
|
+
header to reset).
|
|
158
|
+
- **Server TPS (framework 0.4.2, kernel 0.4.2):** Server > Status has a *TPS* line next to Memory and Lua heap: the
|
|
159
|
+
last minute's average, its slowest second and the physics FPS (`59.9 avg, 52.0 min, physics 60`; the warning colour
|
|
160
|
+
under 50 on average). Older kernels show `needs kernel 0.4.2`. The same numbers ride on fleet heartbeats (the
|
|
161
|
+
explorer's fleet table).
|
|
162
|
+
- **Dev-only tools (framework 0.4.1, `devtools/access.ts`):** Claude, Logs > Upload, Dex edits on the server, Network
|
|
163
|
+
packet blocking and the hot-swap sound work under dev rules (private, reserved and Studio servers on a dev branch)
|
|
164
|
+
for every dev, and on a public server an owner switched to a dev branch (Server > Branch) for owners only. Everywhere
|
|
165
|
+
else the menu is read-only, with a short reason: "Dev branch only", "Owners only on public servers", "Owner-switched
|
|
166
|
+
servers only". The rules themselves don't change (`TypeTorch.channel`, signatures, owner-only rollbacks).
|
|
157
167
|
- **Panes and windows (dev menu):** every page shows in a pane with a small header: tap the title to pick another
|
|
158
168
|
page, or use Split right, Split down, Open in new window and Close. Drag the line between two panes to resize them.
|
|
159
169
|
The sidebar opens pages in the focused pane (the highlighted one; tap a pane to focus it). Floating windows move by
|
|
@@ -171,8 +181,8 @@ export class CoinService extends Module implements OnStart {
|
|
|
171
181
|
strings (cut), numbers, booleans, nil, datatypes through `tostring`, an Instance as its full path, a Player as its
|
|
172
182
|
name, `function`, `thread`, buffer sizes. Nothing is ever called (raw reads only: no metamethods, no getters); a
|
|
173
183
|
value that is its own ancestor shows as a cycle. A key filter (open nodes stay listed), Refresh and Auto (every 2 s).
|
|
174
|
-
Server state comes from the op `state.inspect` (rate-limited, size-capped):
|
|
175
|
-
|
|
184
|
+
Server state comes from the op `state.inspect` (rate-limited, size-capped): owners everywhere, other devs where the
|
|
185
|
+
dev-only tools are open to them, since server state can hold player data.
|
|
176
186
|
- **Logs > Upload:** sends the logs shown (Server: this server's log ring; Client: your own client's; Others: the
|
|
177
187
|
picked player's client) to the dev PC paired in the Claude tab, which saves them under `<repo>/.typetorch/logs/`.
|
|
178
188
|
No Claude run, no prompt quota. Not paired: "Pair in the Claude tab first".
|
|
@@ -182,11 +192,12 @@ export class CoinService extends Module implements OnStart {
|
|
|
182
192
|
verified head). A public switch lasts for that server's lifetime and is never stored, so new servers still boot the
|
|
183
193
|
signed prod head. Others see "Join", which moves only them to a reserved server. Manage > Servers "Load a build..."
|
|
184
194
|
does the same for this server, the ticked servers or a share of one branch (A/B pins), with one status line under
|
|
185
|
-
the list.
|
|
195
|
+
the list. A public server an owner switched to a dev branch opens the dev-only tools (Claude too) to owners.
|
|
186
196
|
- **Owners and devs:** two roles (no admins: an older kernel's or typetorch.json's "admin" counts as a dev). The Manage
|
|
187
197
|
group (Players, Servers, Bans) is for owners only.
|
|
188
|
-
- **Remote Claude** (dev-
|
|
189
|
-
|
|
198
|
+
- **Remote Claude** (a dev-only tool: dev rules, or owners on a public server an owner switched to a dev branch):
|
|
199
|
+
while `typetorch remote-claude` (`@typetorch/dev-server`) runs on a dev's machine, allowlisted devs prompt Claude
|
|
200
|
+
Code from the Claude tab. Each dev pairs once by pasting the pairing code printed by
|
|
190
201
|
typetorch-dev-server; the game server keeps the session URL and the tokens in memory and never sends them to clients.
|
|
191
202
|
No Roblox secret is needed.
|
|
192
203
|
|
|
@@ -201,6 +212,10 @@ leaves one behind. Each `on*` returns a disconnect function, which a trove can o
|
|
|
201
212
|
TypeTorch.artifact; // { id, commit, commitHash, branch, channel, builtAt, seq, assetId }
|
|
202
213
|
TypeTorch.generation; // 1, 2, 3... per server (or client)
|
|
203
214
|
TypeTorch.branch; TypeTorch.channel; TypeTorch.serverType; // "public" | "private" | "reserved" | "studio"
|
|
215
|
+
// Kernel 0.3.9: `channel` is the RULES this server runs under ("prod" on every public server; split data stores by it);
|
|
216
|
+
// `branchChannel` is what the branch IS ("prod" for the default branch or one configured prod, else "dev");
|
|
217
|
+
// `rules` is the same as `channel`, by name.
|
|
218
|
+
TypeTorch.branchChannel; TypeTorch.rules;
|
|
204
219
|
TypeTorch.isPinned(); TypeTorch.kernelVersion; TypeTorch.kernelApi; TypeTorch.jobId; TypeTorch.isStudio;
|
|
205
220
|
|
|
206
221
|
// How this generation started: a branch change starts a new generation.
|
|
@@ -420,6 +435,17 @@ const variant = analytics.experiment("onboarding", ["short", "long"]); // same a
|
|
|
420
435
|
`flushSeconds` (15) or at 500 rows, at most ~10 HttpService requests a minute, retries with backoff, and a last
|
|
421
436
|
flush on shutdown (kernel 0.3.2). Delivery is at least once (a swap mid-request sends that batch again).
|
|
422
437
|
`analytics.stats()` (server) has the counters; `flush()` sends soon.
|
|
438
|
+
- **When uploads fail** (a stale quick-tunnel URL, the dev PC's server or tunnel down, a wrong token, HttpService off;
|
|
439
|
+
Roblox reports most network problems as `HttpError: NetFail`, `DnsResolve` or `ConnectFail`): the server backs off
|
|
440
|
+
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
|
|
441
|
+
about 5 minutes, and a settings change retries at once), keeps the rows, and logs at most ONE line a minute: the
|
|
442
|
+
reason and the fix (`analytics upload failed 5 times in a row: NetFail: the connection broke mid-request. ... Retrying
|
|
443
|
+
in 80 s.`, with "(+N more in the last minute)" for the lines it held back) and one line when uploads work again.
|
|
444
|
+
`stats()` also has `failures` (in a row), `failed` (since the server started), `lastError`, `lastStatus`
|
|
445
|
+
(0 = no answer), `lastErrorAt` / `lastOkAt` (`os.time()`), `retryIn`, and, while failing, `reason` and `fix`. The dev
|
|
446
|
+
menu shows them: **Server > Status** has an *Analytics* line (queued, sent, `FAILING x5: ... retry in 80 s`) and a
|
|
447
|
+
*Fleet API* line (the kernel's sender: sent, failed, last error and its age), and **Attention** lists the reason and
|
|
448
|
+
the fix; `typetorch doctor` tests the address and token in the settings record.
|
|
423
449
|
- **The cloud test sends nothing.** `typetorch test --cloud` boots the payload headless in a Luau Execution task where
|
|
424
450
|
HttpService works; there the engine (stub kernel `test = true`, or `workspace:GetAttribute("TypeTorchTest")`) collects
|
|
425
451
|
as usual but never uploads: no event rows, no identity rows, no HTTP request, so a prod deploy never puts a fake
|
|
@@ -431,6 +457,13 @@ const variant = analytics.experiment("onboarding", ["short", "long"]); // same a
|
|
|
431
457
|
- **Friends in the server** (the join's `friends`): one `Players:GetFriendsAsync` per join (at most 10 pages, none
|
|
432
458
|
when the player is alone) intersected with the players there, not a web call per player.
|
|
433
459
|
|
|
460
|
+
**Settings, framework 0.4.0 / kernel 0.4.0 (plans/21):** the record's `backend` = `{ url, key, analytics?: { flushSeconds,
|
|
461
|
+
recordShare } }` (`typetorch backend setup`) is the whole sink: DuckDB at `<url>/v1/ingest` with the one key (Basin
|
|
462
|
+
identities to `<url>/v1/identity`). The old `analytics` value below is read while `backend` is missing, and stays the
|
|
463
|
+
way to a Basin sink. The engine also tells the kernel each player's pid (`setAnalyticsId`: the kernel's error reports
|
|
464
|
+
count affected players by pid, never by name) and counts its own DataStore and HTTP requests for the budget view (dev
|
|
465
|
+
menu **Server > Budget**: bars per Roblox limit for the player count, who spends it, the DataStore budget left, memory).
|
|
466
|
+
|
|
434
467
|
**Settings** (server only, never sent to clients): the signed settings record's `analytics` field (kernel 0.3.8,
|
|
435
468
|
plans/20), written with `typetorch settings set analytics -` (JSON on stdin; `@typetorch/analytics`' `writeSettings()`
|
|
436
469
|
and `bun run local` call it). Every new copy applies live within seconds (the CLI pings servers). Before kernel 0.3.8
|
|
@@ -481,9 +514,11 @@ bun run build # rbxtsc --type package -> out/
|
|
|
481
514
|
shorthand, setCallback with values and Promises, predict, a swap, and the client side (invoke, invokeWithTimeout,
|
|
482
515
|
`createClient({ defaultTimeout })`).
|
|
483
516
|
`scripts/test-analytics.luau` checks the analytics engine's pure parts (experiment assignment, settings, the queue
|
|
484
|
-
and HTTP budget, tt-rec-1, the sink request bodies); `test-analytics-server.luau` and
|
|
485
|
-
run the compiled server and client cores against mocked services (sessions, intake,
|
|
486
|
-
in flight, shutdown; the recorder,
|
|
517
|
+
and HTTP budget, tt-rec-1, the sink request bodies, the failure help); `test-analytics-server.luau` and
|
|
518
|
+
`test-analytics-client.luau` run the compiled server and client cores against mocked services (sessions, intake,
|
|
519
|
+
retries, a NetFail streak and its one log line a minute, a swap with a request in flight, shutdown; the recorder,
|
|
520
|
+
screens, batching). `scripts/test-net-health.luau` checks the dev menu's Fleet API / Analytics status lines and
|
|
521
|
+
Attention issues.
|
|
487
522
|
- **Publishing:** `npm publish` runs `prepublishOnly` (clean + build). The package ships only `out/` (no
|
|
488
523
|
`.tsbuildinfo`), `README.md` and `LICENSE`; check with `bun pm pack --dry-run`.
|
|
489
524
|
|
package/out/analytics/SCHEMA.md
CHANGED
|
@@ -26,7 +26,7 @@ Integers are written with every digit (never exponent notation).
|
|
|
26
26
|
| `seq` | int64 | That artifact's deploy seq (`0` when unknown) |
|
|
27
27
|
| `branch` | string | Server branch |
|
|
28
28
|
| `channel` | string | Effective channel: `prod` or `dev` |
|
|
29
|
-
| `dev` | string | `desktop`, `phone`, `tablet`, `console`, `vr`, `unknown
|
|
29
|
+
| `dev` | string | The session's device class: `desktop`, `phone`, `tablet`, `console`, `vr`, `unknown`. Set once per session from the client's first hello (`unknown` for server-only events and until the client reports; frameworks after 0.4.2 keep it when a later hello says otherwise). How it is decided: the `session` / `device` row below |
|
|
30
30
|
| `newp` | bool | The event belongs to the player's first-ever session |
|
|
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 |
|
|
@@ -58,7 +58,7 @@ First-ever-session detail, one row per packed chunk.
|
|
|
58
58
|
| kind | name | props |
|
|
59
59
|
|---|---|---|
|
|
60
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
|
-
| session | `device` (src client) | `input` (`kbm`, `touch`, `gamepad`, `vr`, `unknown`), `w`, `h` (viewport points), `touch`, `kb`, `mouse`, `pad`, `vr`. Once per session |
|
|
61
|
+
| session | `device` (src client) | `dev?` (the device class, as the `dev` column; frameworks after 0.4.2, older ones leave it out), `input` (`kbm`, `touch`, `gamepad`, `vr`, `unknown`), `w`, `h` (viewport points), `touch`, `kb`, `mouse`, `pad`, `vr`. Once per session. The class (Roblox gives game scripts no OS): `vr` when VRService.VREnabled; `console` when GuiService:IsTenFootInterface(); touch without a keyboard: `tablet` when the viewport's short side is 600 points or more, else `phone` (the first hello waits up to 2 s for a real viewport); a keyboard or mouse: `desktop` (touch laptops, tablets with a keyboard); else `unknown` |
|
|
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) |
|
|
64
64
|
| tech | `client` (src client) | Every `techEvery` s: `fps`, `mem` (MB, 10s), `ping?` (ms) |
|
|
@@ -176,8 +176,13 @@ Unknown fields are ignored. Settings errors are warned once (never with the toke
|
|
|
176
176
|
- **Batches:** every `flushSeconds`, or at 500 queued events or 20 recordings. At most 500 event rows (fleet rows
|
|
177
177
|
first) and 50 recording rows per request, about 900 KB each. At most ~10 requests a minute per server (burst 3).
|
|
178
178
|
- **Answers:** 2xx sent; 413 the batch is halved and resent (a single row is dropped); 401/403/404 rows are kept and
|
|
179
|
-
sending waits 300 s (or until the settings change); 408/429/5xx/no answer retry with
|
|
180
|
-
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`.
|
|
181
186
|
- **Delivery is at least once:** a hot swap during a request sends its rows again. Readers may drop exact duplicate
|
|
182
187
|
rows.
|
|
183
188
|
- **The cloud test sends nothing:** inside `typetorch test --cloud` (the stub kernel's `test = true`, or the workspace
|
|
@@ -26,6 +26,7 @@ local CHUNK_SECONDS = _protocol.CHUNK_SECONDS
|
|
|
26
26
|
local CHUNK_TARGET_BYTES = _protocol.CHUNK_TARGET_BYTES
|
|
27
27
|
local CLIENT_BATCH_BYTES = _protocol.CLIENT_BATCH_BYTES
|
|
28
28
|
local CLIENT_BATCH_MAX = _protocol.CLIENT_BATCH_MAX
|
|
29
|
+
local deviceClass = _protocol.deviceClass
|
|
29
30
|
local isEventName = _protocol.isEventName
|
|
30
31
|
local _props = TS.import(script, script.Parent, "props")
|
|
31
32
|
local encodeProps = _props.encodeProps
|
|
@@ -60,23 +61,28 @@ local IGNORED_GUIS = {
|
|
|
60
61
|
}
|
|
61
62
|
--* A screen that closes and opens again within this many seconds (ResetOnSpawn) never left.
|
|
62
63
|
local SCREEN_DEBOUNCE = 1
|
|
63
|
-
|
|
64
|
+
--* The camera's viewport in points (0 x 0 without a camera).
|
|
65
|
+
local function viewportSize()
|
|
64
66
|
local camera = Workspace.CurrentCamera
|
|
65
|
-
|
|
67
|
+
return if camera then camera.ViewportSize else Vector2.new(0, 0)
|
|
68
|
+
end
|
|
69
|
+
--* Seconds the first hello waits for a real viewport (a camera reports 1 x 1 for a moment at startup).
|
|
70
|
+
local VIEWPORT_WAIT = 2
|
|
71
|
+
local function deviceInfo()
|
|
72
|
+
local viewport = viewportSize()
|
|
66
73
|
local vr = VRService.VREnabled
|
|
67
74
|
local consoleOk, isConsole = pcall(function()
|
|
68
75
|
return GuiService:IsTenFootInterface()
|
|
69
76
|
end)
|
|
70
|
-
local dev =
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
end
|
|
77
|
+
local dev = deviceClass({
|
|
78
|
+
vr = vr,
|
|
79
|
+
tenFoot = consoleOk and isConsole == true,
|
|
80
|
+
touch = UserInputService.TouchEnabled,
|
|
81
|
+
keyboard = UserInputService.KeyboardEnabled,
|
|
82
|
+
mouse = UserInputService.MouseEnabled,
|
|
83
|
+
w = viewport.X,
|
|
84
|
+
h = viewport.Y,
|
|
85
|
+
})
|
|
80
86
|
local last = UserInputService:GetLastInputType()
|
|
81
87
|
local input = "unknown"
|
|
82
88
|
if vr then
|
|
@@ -698,7 +704,7 @@ do
|
|
|
698
704
|
end
|
|
699
705
|
end)
|
|
700
706
|
if not ok then
|
|
701
|
-
warn("[src/analytics/client.ts:
|
|
707
|
+
warn("[src/analytics/client.ts:518]", `[analytics] server message failed: {string.sub(tostring(err), 1, 200)}`)
|
|
702
708
|
end
|
|
703
709
|
end)
|
|
704
710
|
trove:add(function()
|
|
@@ -805,6 +811,11 @@ do
|
|
|
805
811
|
end
|
|
806
812
|
function ClientAnalytics:startSending()
|
|
807
813
|
self.trove:add(task.spawn(function()
|
|
814
|
+
-- The device class reads the viewport (phone vs tablet): give the camera a moment to report a real size.
|
|
815
|
+
local waitUntil = os.clock() + VIEWPORT_WAIT
|
|
816
|
+
while math.min(viewportSize().X, viewportSize().Y) <= 1 and os.clock() < waitUntil and not self.stopped do
|
|
817
|
+
task.wait(0.1)
|
|
818
|
+
end
|
|
808
819
|
-- Hello until the server answers (its engine may start later than this one): every 5 s for a minute, then
|
|
809
820
|
-- every 30 s.
|
|
810
821
|
local tries = 0
|
|
@@ -839,7 +850,7 @@ do
|
|
|
839
850
|
end
|
|
840
851
|
function ClientAnalytics:track(kind, name, props)
|
|
841
852
|
if not isEventName(name) then
|
|
842
|
-
warn("[src/analytics/client.ts:
|
|
853
|
+
warn("[src/analytics/client.ts:637]", `[analytics] {kind} event name must be 1-64 characters: {string.sub(tostring(name), 1, 80)}`)
|
|
843
854
|
return nil
|
|
844
855
|
end
|
|
845
856
|
self:push(kind, name, encodeProps(props))
|
|
@@ -897,7 +908,7 @@ do
|
|
|
897
908
|
end
|
|
898
909
|
local control = _condition
|
|
899
910
|
if not isExperimentName(name) or not isVariantList(variants) then
|
|
900
|
-
warn("[src/analytics/client.ts:
|
|
911
|
+
warn("[src/analytics/client.ts:671]", `[analytics] experiment("{string.sub(tostring(name), 1, 64)}"): bad name or variants (1-16 distinct names)`)
|
|
901
912
|
return control
|
|
902
913
|
end
|
|
903
914
|
local deadline = os.clock() + 10
|
|
@@ -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
|
+
}
|
|
@@ -35,6 +35,27 @@ export interface HelloInfo {
|
|
|
35
35
|
/** Seconds from the client starting to the engine starting (first client generation only). */
|
|
36
36
|
load?: number;
|
|
37
37
|
}
|
|
38
|
+
/** What the client knows about its device when it says hello (UserInputService, VRService, GuiService, the camera). */
|
|
39
|
+
export interface DeviceFacts {
|
|
40
|
+
vr: boolean;
|
|
41
|
+
/** GuiService:IsTenFootInterface(): a console (Xbox, PlayStation). */
|
|
42
|
+
tenFoot: boolean;
|
|
43
|
+
touch: boolean;
|
|
44
|
+
keyboard: boolean;
|
|
45
|
+
mouse: boolean;
|
|
46
|
+
/** Viewport size in points (0 when the camera has none yet). */
|
|
47
|
+
w: number;
|
|
48
|
+
h: number;
|
|
49
|
+
}
|
|
50
|
+
/** A touch-only device whose viewport's short side is at least this many points is a tablet, else a phone. */
|
|
51
|
+
export declare const TABLET_MIN_SHORT_SIDE = 600;
|
|
52
|
+
/**
|
|
53
|
+
* The session's device class (the `dev` column), in this order: VR headset; console (ten-foot interface); touch without
|
|
54
|
+
* a keyboard: tablet when the viewport's short side is 600 points or more, else phone (also when the viewport isn't
|
|
55
|
+
* known); a keyboard or mouse: desktop (a touch laptop or a tablet with a keyboard counts here); else unknown. Roblox
|
|
56
|
+
* gives game scripts no OS, so this is the closest a game gets.
|
|
57
|
+
*/
|
|
58
|
+
export declare function deviceClass(f: DeviceFacts): DeviceKind;
|
|
38
59
|
/** [ageMs, kind, name, propsJson] */
|
|
39
60
|
export type ClientEvent = [
|
|
40
61
|
age: number,
|
|
@@ -22,6 +22,32 @@ local CLIENT_BATCH_BYTES = 12000
|
|
|
22
22
|
local CHUNK_TARGET_BYTES = 6000
|
|
23
23
|
local CHUNK_MAX_BYTES = 16384
|
|
24
24
|
local CHUNK_SECONDS = 10
|
|
25
|
+
--* What the client knows about its device when it says hello (UserInputService, VRService, GuiService, the camera).
|
|
26
|
+
--* A touch-only device whose viewport's short side is at least this many points is a tablet, else a phone.
|
|
27
|
+
local TABLET_MIN_SHORT_SIDE = 600
|
|
28
|
+
--[[
|
|
29
|
+
*
|
|
30
|
+
* The session's device class (the `dev` column), in this order: VR headset; console (ten-foot interface); touch without
|
|
31
|
+
* a keyboard: tablet when the viewport's short side is 600 points or more, else phone (also when the viewport isn't
|
|
32
|
+
* known); a keyboard or mouse: desktop (a touch laptop or a tablet with a keyboard counts here); else unknown. Roblox
|
|
33
|
+
* gives game scripts no OS, so this is the closest a game gets.
|
|
34
|
+
|
|
35
|
+
]]
|
|
36
|
+
local function deviceClass(f)
|
|
37
|
+
if f.vr then
|
|
38
|
+
return "vr"
|
|
39
|
+
end
|
|
40
|
+
if f.tenFoot then
|
|
41
|
+
return "console"
|
|
42
|
+
end
|
|
43
|
+
if f.touch and not f.keyboard then
|
|
44
|
+
return if math.min(f.w, f.h) >= TABLET_MIN_SHORT_SIDE then "tablet" else "phone"
|
|
45
|
+
end
|
|
46
|
+
if f.keyboard or f.mouse then
|
|
47
|
+
return "desktop"
|
|
48
|
+
end
|
|
49
|
+
return "unknown"
|
|
50
|
+
end
|
|
25
51
|
--* [ageMs, kind, name, propsJson]
|
|
26
52
|
local DEVICES = {
|
|
27
53
|
["desktop"] = true,
|
|
@@ -127,6 +153,7 @@ local function cleanClientEvent(value)
|
|
|
127
153
|
return { math.floor(age), kind, name, props }
|
|
128
154
|
end
|
|
129
155
|
return {
|
|
156
|
+
deviceClass = deviceClass,
|
|
130
157
|
cleanHello = cleanHello,
|
|
131
158
|
isEventName = isEventName,
|
|
132
159
|
cleanClientEvent = cleanClientEvent,
|
|
@@ -136,5 +163,6 @@ return {
|
|
|
136
163
|
CHUNK_TARGET_BYTES = CHUNK_TARGET_BYTES,
|
|
137
164
|
CHUNK_MAX_BYTES = CHUNK_MAX_BYTES,
|
|
138
165
|
CHUNK_SECONDS = CHUNK_SECONDS,
|
|
166
|
+
TABLET_MIN_SHORT_SIDE = TABLET_MIN_SHORT_SIDE,
|
|
139
167
|
CLIENT_KINDS = CLIENT_KINDS,
|
|
140
168
|
}
|
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
|
}
|
|
@@ -193,9 +193,25 @@ export interface AnalyticsStats {
|
|
|
193
193
|
dropped: number;
|
|
194
194
|
/** Rows the backend refused (HTTP 4xx). */
|
|
195
195
|
rejected: number;
|
|
196
|
-
/** Failed requests in a row. */
|
|
196
|
+
/** Failed requests in a row (0 after a success). */
|
|
197
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
|
+
*/
|
|
198
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;
|
|
199
215
|
/** Settings problems (never contains the token). */
|
|
200
216
|
settingsErrors: string[];
|
|
201
217
|
}
|
|
@@ -69,6 +69,10 @@ export declare class ServerAnalytics {
|
|
|
69
69
|
private backoffUntil;
|
|
70
70
|
private batchRows;
|
|
71
71
|
private lastWarnAt;
|
|
72
|
+
/** Lines held back by the one-a-minute limit since the last one that was printed. */
|
|
73
|
+
private suppressed;
|
|
74
|
+
/** A failure line was printed since the last recovery line (so a recovery is only announced after an announced failure). */
|
|
75
|
+
private outageLogged;
|
|
72
76
|
private flushing;
|
|
73
77
|
private closing;
|
|
74
78
|
private readonly identity;
|
|
@@ -114,6 +118,13 @@ export declare class ServerAnalytics {
|
|
|
114
118
|
* changes nothing.
|
|
115
119
|
*/
|
|
116
120
|
private resolve;
|
|
121
|
+
/**
|
|
122
|
+
* Kernel 0.4.0 (plans/21 C): the kernel's error reports count affected players by pid (never a name or UserId), so
|
|
123
|
+
* the kernel hears each player's pid. Older kernels: nothing.
|
|
124
|
+
*/
|
|
125
|
+
private tellKernelPid;
|
|
126
|
+
/** Kernel 0.4.0: counts this engine's own requests for the budget view (older kernels: nothing). */
|
|
127
|
+
private countBudget;
|
|
117
128
|
private joinSource;
|
|
118
129
|
/**
|
|
119
130
|
* How many of the player's friends are in this server: one `GetFriendsAsync` per join (at most FRIEND_PAGES_MAX
|
|
@@ -139,7 +150,10 @@ export declare class ServerAnalytics {
|
|
|
139
150
|
private startZones;
|
|
140
151
|
private startFleet;
|
|
141
152
|
private onDeployReport;
|
|
153
|
+
/** At most one log line a minute; the ones held back are counted in the next line. Returns whether it was printed. */
|
|
142
154
|
private warn;
|
|
155
|
+
/** A request was accepted: ends a failure streak, and says so once if the streak was announced. */
|
|
156
|
+
private noteSuccess;
|
|
143
157
|
/** POST one request. Returns the HTTP status (0: no response). Never logs the token or the body. */
|
|
144
158
|
private post;
|
|
145
159
|
/** Sends one batch per table. `final`: shutdown, ignore the budget. */
|