@typetorch/framework 0.5.7 → 0.6.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 +26 -3
- package/out/analytics/SCHEMA.md +6 -2
- package/out/analytics/join.d.ts +69 -0
- package/out/analytics/join.luau +178 -0
- package/out/analytics/rows.d.ts +14 -0
- package/out/analytics/rows.luau +29 -0
- package/out/analytics/schema.d.ts +4 -1
- package/out/analytics/server.d.ts +10 -0
- package/out/analytics/server.luau +216 -29
- package/out/analytics/settings.luau +11 -0
- package/out/devtools/sha256.d.ts +1 -8
- package/out/devtools/sha256.luau +15 -89
- package/out/index.d.ts +1 -0
- package/out/kernel.d.ts +6 -0
- package/out/notifications/api.d.ts +50 -0
- package/out/notifications/api.luau +17 -0
- package/out/notifications/client.d.ts +21 -0
- package/out/notifications/client.luau +201 -0
- package/out/notifications/index.d.ts +8 -0
- package/out/notifications/init.luau +34 -0
- package/out/notifications/protocol.d.ts +146 -0
- package/out/notifications/protocol.luau +519 -0
- package/out/notifications/server.d.ts +73 -0
- package/out/notifications/server.luau +856 -0
- package/out/notifications/token.d.ts +57 -0
- package/out/notifications/token.luau +349 -0
- package/out/runtime/start.luau +5 -2
- package/out/typetorch.d.ts +9 -0
- package/out/typetorch.luau +37 -3
- package/out/util/sha256.d.ts +8 -0
- package/out/util/sha256.luau +91 -0
- package/package.json +14 -3
package/README.md
CHANGED
|
@@ -360,6 +360,27 @@ TypeTorch.runDetached(() => store.StartSessionAsync(`Player_${userId}`)).then((p
|
|
|
360
360
|
run at once per server (then it throws); one past 60 s is logged and flagged in Server > Status. Older kernels
|
|
361
361
|
throw "needs kernel 0.3.8; use the DataHost job queue (Player data guide)": the other option, which works on every
|
|
362
362
|
kernel. `features.runDetached` tells which one this server has.
|
|
363
|
+
- **`notifications()`** (plans/29): native Roblox experience notifications through the TypeTorch backend (`backend.url`
|
|
364
|
+
/ `backend.key`, the launch-token keys in `backend.notify` from `typetorch notify key rotate`; no kernel change).
|
|
365
|
+
The game says why a player should hear from it, the backend decides when and sends, the framework verifies the tap:
|
|
366
|
+
|
|
367
|
+
```ts
|
|
368
|
+
const notify = TypeTorch.notifications();
|
|
369
|
+
// Server: intents (upsert by user + kind + key; offline players by UserId), batched and swap-safe.
|
|
370
|
+
notify.want(userIdOrPlayer, "gift_new", { key: letterId, until: expiresAt, params: { gift: "5 coins" }, data: `mail:${letterId}` });
|
|
371
|
+
notify.drop(userIdOrPlayer, "streak", "20034", "played"); // no key: every key of the kind
|
|
372
|
+
trove.add(notify.onOpen((player, open) => openMail(player, open.data))); // open: { nid, kind, data, ageSeconds, first }
|
|
373
|
+
// Client: Roblox's opt-in prompt, once per session (show your own explainer first). Yields until it closes.
|
|
374
|
+
const result = TypeTorch.notifications().promptOptIn("streak_2"); // "shown" | "unavailable" | "error" | "busy" | "already"
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Kinds are declared in typetorch.json `notifications` and pushed with the CLI. Intents go to
|
|
378
|
+
`<backend.url>/v1/notify/intents` every `flushSeconds` (and at 200); presence (`online` on join and every 3 h, `offline` on leave)
|
|
379
|
+
rides along while `backend.notify` is set. A join with `tt1n.` launch data is verified on the server (HMAC, the
|
|
380
|
+
player's UserId, 14 days; the previous key for 14 days after a rotation): `onOpen` gets only verified data, also for
|
|
381
|
+
players who joined before the callback connected. Rows (`kind = notify`): `want`, `drop`, `optin_check`,
|
|
382
|
+
`optin_shown`, `optin_closed`, `open`, `open_bad`, `played` (src/analytics/SCHEMA.md); the join row gets
|
|
383
|
+
`from = "notification"`. Without backend settings (Studio) nothing is posted; rows are still written.
|
|
363
384
|
- **Edit mode** (UI Labs stories, no kernel): `running` is false, identity has defaults, `persist` keeps a local table,
|
|
364
385
|
events never fire, and the server-only reads throw.
|
|
365
386
|
|
|
@@ -520,7 +541,7 @@ const variant = analytics.experiment("onboarding", ["short", "long"]); // same a
|
|
|
520
541
|
when the player is alone) intersected with the players there, not a web call per player.
|
|
521
542
|
|
|
522
543
|
**Settings, framework 0.4.0 / kernel 0.4.0 (plans/21):** the record's `backend` = `{ url, key, analytics?: { flushSeconds,
|
|
523
|
-
recordShare } }` (`typetorch backend setup`) is the whole sink: DuckDB at `<url>/v1/ingest` with the one key (Basin
|
|
544
|
+
recordShare, joinLaunchPrefix } }` (`typetorch backend setup`) is the whole sink: DuckDB at `<url>/v1/ingest` with the one key (Basin
|
|
524
545
|
identities to `<url>/v1/identity`). The old `analytics` value below is read while `backend` is missing, and stays the
|
|
525
546
|
way to a Basin sink. The engine also tells the kernel each player's pid (`setAnalyticsId`: the kernel's error reports
|
|
526
547
|
count affected players by pid, never by name) and counts its own DataStore and HTTP requests for the budget view (dev
|
|
@@ -589,8 +610,10 @@ bun run build # rbxtsc --type package -> out/
|
|
|
589
610
|
vouch, live-copy rules), `test-messaging` (messaging and the server list), `test-settings` (`liveConfig` and the
|
|
590
611
|
settings lines), `test-detached` (`runDetached`), `test-dev-access` (the dev-only tools rule and every gate through
|
|
591
612
|
it), `test-remote-debug` (the remote debug ops and their limits), `test-net-inspect` (packet capture),
|
|
592
|
-
`test-explorer-core` (Dex), `test-toolbox-sanitize` (the Creator Store sanitizer)
|
|
593
|
-
fingerprint)
|
|
613
|
+
`test-explorer-core` (Dex), `test-toolbox-sanitize` (the Creator Store sanitizer), `test-sha256` (SHA-256 / HMAC in util and the
|
|
614
|
+
pairing-code fingerprint), `test-notifications` (the launch token with the contract's shared vector, intent checks,
|
|
615
|
+
the queue: batches of 200, rev order, a swap's handover), `test-notify-server` (the notifications server against
|
|
616
|
+
mocked services) and `test-analytics-join` (the join row's metadata; no other player's UserId). `node scripts/check-self-calls.mjs out` flags compiled functions that call themselves on their first
|
|
594
617
|
lines (after `bun run build`).
|
|
595
618
|
- **Package contents:** only `out/` (no `.tsbuildinfo`), `README.md` and `LICENSE`; check with `bun pm pack --dry-run`.
|
|
596
619
|
- **Explorer class icons:** `assets/class-icons.png` (kept locally, not in the repo: it is Roblox's own texture) is
|
package/out/analytics/SCHEMA.md
CHANGED
|
@@ -15,7 +15,7 @@ Integers are written with every digit (never exponent notation).
|
|
|
15
15
|
|---|---|---|
|
|
16
16
|
| `v` | int32 | Row format version: `1` |
|
|
17
17
|
| `t` | int64 | Unix ms on the game server's clock. Client events are corrected by the server (`server now - age - ping/2`) |
|
|
18
|
-
| `kind` | string | `session`, `tech`, `zone`, `funnel`, `purchase`, `currency`, `state`, `experiment`, `custom`, `recording_meta`, `fleet` |
|
|
18
|
+
| `kind` | string | `session`, `tech`, `zone`, `funnel`, `purchase`, `currency`, `state`, `experiment`, `custom`, `recording_meta`, `fleet`, `notify` |
|
|
19
19
|
| `name` | string | Event name (1-64 chars), see below |
|
|
20
20
|
| `pid` | string | Random player id (32 hex chars; `t` + 31 hex = temporary, the player-id store couldn't be read). `""` for server-only events. Never the UserId |
|
|
21
21
|
| `sid` | string | Session id (32 hex), new per join, kept across hot swaps. `""` for server-only events |
|
|
@@ -57,8 +57,11 @@ 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
|
|
60
|
+
| session | `join` | `from`: `direct` (Home, search, sorts: Roblox doesn't say), `teleport` (same game), `teleport_game` (another game), `referral` (invite/referral link), `notification` (a TypeTorch notification's launch data verified), `share` (other launch data), `follow` (followed someone); join metadata (plans/29): `src_place?`, `src_game?` (SourcePlaceId / SourceGameId), `ref?` / `follow?` (the referrer / followed user as their pid when this game knows them, else `"other"`: never a UserId; players in the server are free, others cost one DataStore read each, at most 2 per join and only with read budget to spare), `party?` (players teleported along) + `party_pids?` (the pids of those this game knows, at most 10), `ctx?`, `ctx_item?`, `ctx_asset?`, `ctx_outfit?`, `ctx_asset_type?` (GameJoinContext JoinSource / ItemType / AssetId / OutfitId / AssetType), `launch?` (`notification` + `nid`, `nk` (kind); `tt_bad` + `launch_bad` (`format`, `sig`, `user`, `old`); or the game's own launch data: `launch_len`, plus its first 32 chars in `launch` only with the settings' `joinLaunchPrefix`), `tp?` + `tp_bytes?` (TeleportData present, its JSON size; never the content), `age_days` (exact account age), `locale?`, `verified?` (HasVerifiedBadge), `membership` (MembershipType name); `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) | `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
|
+
| notify | `want`, `drop` | Notifications (plans/29), written by the game server: `k` (kind), `key?` (first 12 hex of SHA-256 of the intent's key; never the key), `at?`, `until?`, `offline` (want: the user isn't in this server; the row then has no pid), `why?` (drop) |
|
|
63
|
+
| notify | `optin_check`, `optin_shown`, `optin_closed` | The client's opt-in prompt, checked and written by the server (at most 3 of each per session): `where`, `can` (check), `err?` (`timeout`, `swap` or the error's first 64 chars), `secs` (closed: how long it was up) |
|
|
64
|
+
| notify | `open`, `open_bad`, `played` | A join from a notification: `open` (`nid`, `k`, `age` (secs since sent), `first`), `open_bad` (`why`: `format`, `sig`, `user`, `old`), `played` (`nid`, `k`, `secs`: the opened session's length, at its end). Backend rows of the same kind (`planned`, `sent`, ...) come from `src = "backend"` |
|
|
62
65
|
| 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
66
|
| 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
67
|
| tech | `client` (src client) | Every `techEvery` s: `fps`, `mem` (MB, 10s), `ping?` (ms) |
|
|
@@ -159,6 +162,7 @@ repo's `bun run local` does it). Every new signed copy applies at once, so dials
|
|
|
159
162
|
| `experiments` | none | Per experiment: `active: false` (everyone gets the first variant, not stamped), `weights` (per variant, in the game's order), `variant` (force one), `players` (`{ [UserId or pid]: variant }`, at most 200: forces one player's variant over everything else, stamped `forced`; each client's hello gets only its own entry). The CLI writes them: `typetorch experiment` |
|
|
160
163
|
| `identity` | the settings' `fleet` url + `/v1/identity` | basin: where identity rows go (below); none and no `fleet` in the settings: not sent. duckdb: unused |
|
|
161
164
|
| `identityToken` | the settings' `fleet` token | basin: the token for `identity`. Never logged |
|
|
165
|
+
| `joinLaunchPrefix` | false | true: the join row's `launch` keeps the first 32 characters of the game's own launch data (off: only `launch_len`). Set it in `backend.analytics` (or the old `analytics` value) |
|
|
162
166
|
|
|
163
167
|
Unknown fields are ignored. Settings errors are warned once (never with the token).
|
|
164
168
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `session` / `join` row's join metadata (plans/29 "Native join metadata"): every field of `Player:GetJoinData()`
|
|
3
|
+
* and the player facts, as props. The engine reads the data (enum values as their names, the TeleportData's JSON size,
|
|
4
|
+
* other players' pids looked up beforehand) and this builds the props.
|
|
5
|
+
*
|
|
6
|
+
* Privacy: no other player's UserId ever lands in a prop. Referrer, followed user and party members become pids (the
|
|
7
|
+
* game's pseudonymous ids) when this game knows them, else "other" (ref / follow) or nothing (party_pids).
|
|
8
|
+
*
|
|
9
|
+
* Pure: no imports and no services, so it runs under Lune (scripts/test-analytics-join.luau).
|
|
10
|
+
*/
|
|
11
|
+
/** What the engine reads from GetJoinData (enums already turned into names). */
|
|
12
|
+
export interface JoinDataLike {
|
|
13
|
+
SourcePlaceId?: number;
|
|
14
|
+
SourceGameId?: number;
|
|
15
|
+
ReferredByPlayerId?: number;
|
|
16
|
+
Members?: number[];
|
|
17
|
+
/** Whether TeleportData was present (its content never leaves the server). */
|
|
18
|
+
HasTeleportData?: boolean;
|
|
19
|
+
LaunchData?: string;
|
|
20
|
+
GameJoinContext?: {
|
|
21
|
+
JoinSource?: string;
|
|
22
|
+
ItemType?: string;
|
|
23
|
+
AssetId?: string;
|
|
24
|
+
OutfitId?: string;
|
|
25
|
+
AssetType?: string;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** The joining player and this server. */
|
|
29
|
+
export interface JoinFacts {
|
|
30
|
+
userId: number;
|
|
31
|
+
/** game.GameId (a teleport from another game is `teleport_game`). */
|
|
32
|
+
gameId: number;
|
|
33
|
+
followUserId: number;
|
|
34
|
+
/** Player.AccountAge (days). */
|
|
35
|
+
accountAge: number;
|
|
36
|
+
locale?: string;
|
|
37
|
+
verified?: boolean;
|
|
38
|
+
/** Enum.MembershipType name. */
|
|
39
|
+
membership?: string;
|
|
40
|
+
/** The TeleportData's JSON size in bytes (undefined: none or not encodable). */
|
|
41
|
+
tpBytes?: number;
|
|
42
|
+
}
|
|
43
|
+
/** The launch data's verdict (notifications/token.ts verifyToken): undefined when it isn't a TypeTorch token. */
|
|
44
|
+
export type LaunchVerdict = {
|
|
45
|
+
ok: true;
|
|
46
|
+
nid: string;
|
|
47
|
+
kind: string;
|
|
48
|
+
} | {
|
|
49
|
+
ok: false;
|
|
50
|
+
why: string;
|
|
51
|
+
} | undefined;
|
|
52
|
+
export interface JoinOptions {
|
|
53
|
+
/**
|
|
54
|
+
* Keep the first LAUNCH_KEEP chars of the game's own launch data in `launch` (the analytics settings'
|
|
55
|
+
* `joinLaunchPrefix`, default off: launch data can carry anything a link maker put there). `launch_len` is always kept.
|
|
56
|
+
*/
|
|
57
|
+
launchPrefix?: boolean;
|
|
58
|
+
}
|
|
59
|
+
/** Party pids kept at most. */
|
|
60
|
+
export declare const PARTY_PIDS_MAX = 10;
|
|
61
|
+
/** The game's own launch data: this many characters are kept. */
|
|
62
|
+
export declare const LAUNCH_KEEP = 32;
|
|
63
|
+
/** UserIds whose pids the row wants (referrer, followed user, party members), without the player and duplicates. */
|
|
64
|
+
export declare function wantedUserIds(data: JoinDataLike | undefined, facts: JoinFacts): number[];
|
|
65
|
+
/**
|
|
66
|
+
* The join props. `pidOf(userId)` answers a known pid (a player in this server, or one this game's records know) or
|
|
67
|
+
* undefined.
|
|
68
|
+
*/
|
|
69
|
+
export declare function joinProps(data: JoinDataLike | undefined, facts: JoinFacts, pidOf: (userId: number) => string | undefined, launch: LaunchVerdict, options?: JoinOptions): Record<string, unknown>;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
--[[
|
|
3
|
+
*
|
|
4
|
+
* The `session` / `join` row's join metadata (plans/29 "Native join metadata"): every field of `Player:GetJoinData()`
|
|
5
|
+
* and the player facts, as props. The engine reads the data (enum values as their names, the TeleportData's JSON size,
|
|
6
|
+
* other players' pids looked up beforehand) and this builds the props.
|
|
7
|
+
*
|
|
8
|
+
* Privacy: no other player's UserId ever lands in a prop. Referrer, followed user and party members become pids (the
|
|
9
|
+
* game's pseudonymous ids) when this game knows them, else "other" (ref / follow) or nothing (party_pids).
|
|
10
|
+
*
|
|
11
|
+
* Pure: no imports and no services, so it runs under Lune (scripts/test-analytics-join.luau).
|
|
12
|
+
|
|
13
|
+
]]
|
|
14
|
+
--* What the engine reads from GetJoinData (enums already turned into names).
|
|
15
|
+
--* The joining player and this server.
|
|
16
|
+
--* The launch data's verdict (notifications/token.ts verifyToken): undefined when it isn't a TypeTorch token.
|
|
17
|
+
--* Party pids kept at most.
|
|
18
|
+
local PARTY_PIDS_MAX = 10
|
|
19
|
+
--* The game's own launch data: this many characters are kept.
|
|
20
|
+
local LAUNCH_KEEP = 32
|
|
21
|
+
--* UserIds whose pids the row wants (referrer, followed user, party members), without the player and duplicates.
|
|
22
|
+
local function wantedUserIds(data, facts)
|
|
23
|
+
local seen = {}
|
|
24
|
+
local list = {}
|
|
25
|
+
local add = function(userId)
|
|
26
|
+
local _userId = userId
|
|
27
|
+
local _condition = not (type(_userId) == "number")
|
|
28
|
+
if not _condition then
|
|
29
|
+
_condition = userId <= 0 or userId == facts.userId
|
|
30
|
+
if not _condition then
|
|
31
|
+
local _userId_1 = userId
|
|
32
|
+
_condition = seen[_userId_1] ~= nil
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
if _condition then
|
|
36
|
+
return nil
|
|
37
|
+
end
|
|
38
|
+
local _userId_1 = userId
|
|
39
|
+
seen[_userId_1] = true
|
|
40
|
+
local _userId_2 = userId
|
|
41
|
+
table.insert(list, _userId_2)
|
|
42
|
+
end
|
|
43
|
+
local _result = data
|
|
44
|
+
if _result ~= nil then
|
|
45
|
+
_result = _result.ReferredByPlayerId
|
|
46
|
+
end
|
|
47
|
+
add(_result)
|
|
48
|
+
add(facts.followUserId)
|
|
49
|
+
local _result_1 = data
|
|
50
|
+
if _result_1 ~= nil then
|
|
51
|
+
_result_1 = _result_1.Members
|
|
52
|
+
end
|
|
53
|
+
if _result_1 ~= nil then
|
|
54
|
+
for _, member in data.Members do
|
|
55
|
+
add(member)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
return list
|
|
59
|
+
end
|
|
60
|
+
--[[
|
|
61
|
+
*
|
|
62
|
+
* The join props. `pidOf(userId)` answers a known pid (a player in this server, or one this game's records know) or
|
|
63
|
+
* undefined.
|
|
64
|
+
|
|
65
|
+
]]
|
|
66
|
+
local function joinProps(data, facts, pidOf, launch, options)
|
|
67
|
+
if options == nil then
|
|
68
|
+
options = {}
|
|
69
|
+
end
|
|
70
|
+
local info = {}
|
|
71
|
+
local from = "direct"
|
|
72
|
+
local mapped = function(userId)
|
|
73
|
+
local _condition = pidOf(userId)
|
|
74
|
+
if _condition == nil then
|
|
75
|
+
_condition = "other"
|
|
76
|
+
end
|
|
77
|
+
return _condition
|
|
78
|
+
end
|
|
79
|
+
if data ~= nil then
|
|
80
|
+
if data.SourcePlaceId ~= nil and data.SourcePlaceId ~= 0 then
|
|
81
|
+
from = if data.SourceGameId ~= nil and data.SourceGameId ~= facts.gameId then "teleport_game" else "teleport"
|
|
82
|
+
info.src_place = data.SourcePlaceId
|
|
83
|
+
if data.SourceGameId ~= nil and data.SourceGameId ~= 0 then
|
|
84
|
+
info.src_game = data.SourceGameId
|
|
85
|
+
end
|
|
86
|
+
elseif data.ReferredByPlayerId ~= nil and data.ReferredByPlayerId ~= 0 then
|
|
87
|
+
from = "referral"
|
|
88
|
+
elseif launch ~= nil and launch.ok then
|
|
89
|
+
from = "notification"
|
|
90
|
+
elseif data.LaunchData ~= nil and data.LaunchData ~= "" then
|
|
91
|
+
from = "share"
|
|
92
|
+
end
|
|
93
|
+
if data.ReferredByPlayerId ~= nil and data.ReferredByPlayerId > 0 then
|
|
94
|
+
info.ref = mapped(data.ReferredByPlayerId)
|
|
95
|
+
end
|
|
96
|
+
local context = data.GameJoinContext
|
|
97
|
+
if context ~= nil then
|
|
98
|
+
if context.JoinSource ~= nil then
|
|
99
|
+
info.ctx = context.JoinSource
|
|
100
|
+
end
|
|
101
|
+
if context.ItemType ~= nil then
|
|
102
|
+
info.ctx_item = context.ItemType
|
|
103
|
+
end
|
|
104
|
+
if context.AssetId ~= nil and context.AssetId ~= "" then
|
|
105
|
+
info.ctx_asset = context.AssetId
|
|
106
|
+
end
|
|
107
|
+
if context.OutfitId ~= nil and context.OutfitId ~= "" then
|
|
108
|
+
info.ctx_outfit = context.OutfitId
|
|
109
|
+
end
|
|
110
|
+
if context.AssetType ~= nil then
|
|
111
|
+
info.ctx_asset_type = context.AssetType
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
if data.HasTeleportData == true then
|
|
115
|
+
info.tp = true
|
|
116
|
+
if facts.tpBytes ~= nil then
|
|
117
|
+
info.tp_bytes = facts.tpBytes
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
local members = data.Members
|
|
121
|
+
if members ~= nil and #members > 0 then
|
|
122
|
+
info.party = #members
|
|
123
|
+
local pids = {}
|
|
124
|
+
for _, member in members do
|
|
125
|
+
if member == facts.userId or #pids >= PARTY_PIDS_MAX then
|
|
126
|
+
continue
|
|
127
|
+
end
|
|
128
|
+
local pid = pidOf(member)
|
|
129
|
+
if pid ~= nil and not (table.find(pids, pid) ~= nil) then
|
|
130
|
+
table.insert(pids, pid)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
if #pids > 0 then
|
|
134
|
+
info.party_pids = pids
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
local launchData = data.LaunchData
|
|
138
|
+
if launch ~= nil then
|
|
139
|
+
if launch.ok then
|
|
140
|
+
info.launch = "notification"
|
|
141
|
+
info.nid = launch.nid
|
|
142
|
+
info.nk = launch.kind
|
|
143
|
+
else
|
|
144
|
+
info.launch = "tt_bad"
|
|
145
|
+
info.launch_bad = launch.why
|
|
146
|
+
end
|
|
147
|
+
elseif launchData ~= nil and launchData ~= "" then
|
|
148
|
+
if options.launchPrefix == true then
|
|
149
|
+
info.launch = string.sub(launchData, 1, LAUNCH_KEEP)
|
|
150
|
+
end
|
|
151
|
+
info.launch_len = #launchData
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
if from == "direct" and facts.followUserId ~= 0 then
|
|
155
|
+
from = "follow"
|
|
156
|
+
end
|
|
157
|
+
if facts.followUserId > 0 then
|
|
158
|
+
info.follow = mapped(facts.followUserId)
|
|
159
|
+
end
|
|
160
|
+
info.from = from
|
|
161
|
+
info.age_days = math.max(0, math.floor(facts.accountAge))
|
|
162
|
+
if facts.locale ~= nil and facts.locale ~= "" then
|
|
163
|
+
info.locale = facts.locale
|
|
164
|
+
end
|
|
165
|
+
if facts.verified ~= nil then
|
|
166
|
+
info.verified = facts.verified
|
|
167
|
+
end
|
|
168
|
+
if facts.membership ~= nil then
|
|
169
|
+
info.membership = facts.membership
|
|
170
|
+
end
|
|
171
|
+
return info
|
|
172
|
+
end
|
|
173
|
+
return {
|
|
174
|
+
wantedUserIds = wantedUserIds,
|
|
175
|
+
joinProps = joinProps,
|
|
176
|
+
PARTY_PIDS_MAX = PARTY_PIDS_MAX,
|
|
177
|
+
LAUNCH_KEEP = LAUNCH_KEEP,
|
|
178
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How built-ins outside the engine (notifications) write rows into the running server engine's events: the engine
|
|
3
|
+
* registers a writer when it starts and removes it when its generation stops (the `status.ts` pattern). Module state is
|
|
4
|
+
* fresh in every generation. Types only, so importing this loads nothing else.
|
|
5
|
+
*
|
|
6
|
+
* `target`: a Player (their session's pid / sid), a UserId (the session of that player when they are in this server;
|
|
7
|
+
* otherwise a server row without a pid: never a raw UserId in `events`), or undefined (a server row).
|
|
8
|
+
*/
|
|
9
|
+
export type BuiltinRowKind = "notify";
|
|
10
|
+
export type BuiltinRowWriter = (target: Player | number | undefined, kind: BuiltinRowKind, name: string, props: object) => void;
|
|
11
|
+
/** Registers the engine's writer; the returned function removes it again (only if it is still the registered one). */
|
|
12
|
+
export declare function registerBuiltinRows(fn: BuiltinRowWriter): () => void;
|
|
13
|
+
/** The running server engine's writer, or undefined when no engine runs in this generation (yet). */
|
|
14
|
+
export declare function builtinRowWriter(): BuiltinRowWriter | undefined;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
--[[
|
|
3
|
+
*
|
|
4
|
+
* How built-ins outside the engine (notifications) write rows into the running server engine's events: the engine
|
|
5
|
+
* registers a writer when it starts and removes it when its generation stops (the `status.ts` pattern). Module state is
|
|
6
|
+
* fresh in every generation. Types only, so importing this loads nothing else.
|
|
7
|
+
*
|
|
8
|
+
* `target`: a Player (their session's pid / sid), a UserId (the session of that player when they are in this server;
|
|
9
|
+
* otherwise a server row without a pid: never a raw UserId in `events`), or undefined (a server row).
|
|
10
|
+
|
|
11
|
+
]]
|
|
12
|
+
local writer
|
|
13
|
+
--* Registers the engine's writer; the returned function removes it again (only if it is still the registered one).
|
|
14
|
+
local function registerBuiltinRows(fn)
|
|
15
|
+
writer = fn
|
|
16
|
+
return function()
|
|
17
|
+
if writer == fn then
|
|
18
|
+
writer = nil
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
--* The running server engine's writer, or undefined when no engine runs in this generation (yet).
|
|
23
|
+
local function builtinRowWriter()
|
|
24
|
+
return writer
|
|
25
|
+
end
|
|
26
|
+
return {
|
|
27
|
+
registerBuiltinRows = registerBuiltinRows,
|
|
28
|
+
builtinRowWriter = builtinRowWriter,
|
|
29
|
+
}
|
|
@@ -9,7 +9,7 @@ export declare const SCHEMA_VERSION = 1;
|
|
|
9
9
|
export declare const RECORDING_CODEC = "tt-rec-1";
|
|
10
10
|
/** Most bytes of the `props` JSON text. Larger props become `{"_trunc":<bytes>}`. */
|
|
11
11
|
export declare const PROPS_MAX_BYTES = 4096;
|
|
12
|
-
export type EventKind = "session" | "tech" | "zone" | "funnel" | "purchase" | "currency" | "state" | "experiment" | "custom" | "recording_meta" | "fleet";
|
|
12
|
+
export type EventKind = "session" | "tech" | "zone" | "funnel" | "purchase" | "currency" | "state" | "experiment" | "custom" | "recording_meta" | "fleet" | "notify";
|
|
13
13
|
export type DeviceKind = "desktop" | "phone" | "tablet" | "console" | "vr" | "unknown";
|
|
14
14
|
/** One row of the events table. Flat; every column is always present. */
|
|
15
15
|
export interface EventRow {
|
|
@@ -119,6 +119,8 @@ export interface AnalyticsSettings {
|
|
|
119
119
|
identity?: string;
|
|
120
120
|
/** basin: the token for `identity` (default: the settings' `fleet` token). Never logged. */
|
|
121
121
|
identityToken?: string;
|
|
122
|
+
/** Keep the first 32 chars of the game's own launch data on the join row (`launch`); default off (`launch_len` always). */
|
|
123
|
+
joinLaunchPrefix?: boolean;
|
|
122
124
|
}
|
|
123
125
|
/** AnalyticsSettings with every default filled in. */
|
|
124
126
|
export interface ResolvedSettings {
|
|
@@ -132,6 +134,7 @@ export interface ResolvedSettings {
|
|
|
132
134
|
experiments: Map<string, ExperimentOverride>;
|
|
133
135
|
identity?: string;
|
|
134
136
|
identityToken?: string;
|
|
137
|
+
joinLaunchPrefix: boolean;
|
|
135
138
|
}
|
|
136
139
|
export type AnalyticsValue = string | number | boolean;
|
|
137
140
|
/** Event properties: a flat JSON object (nested tables work but are harder to query). */
|
|
@@ -94,6 +94,8 @@ export declare class ServerAnalytics {
|
|
|
94
94
|
private requeueEvents;
|
|
95
95
|
/** A "fleet" row (server-only, src server) on the fleet queue. */
|
|
96
96
|
private emitFleet;
|
|
97
|
+
/** A built-in's row (analytics/rows.ts): on the player's session when they are here, else a server row (no pid). */
|
|
98
|
+
private builtinRow;
|
|
97
99
|
/** The player's session; created now if game code is faster than the engine's own join handler. */
|
|
98
100
|
sessionOf(player: Player): PlayerSession | undefined;
|
|
99
101
|
/** Game code's events. `player` undefined: a server-only event (no pid). */
|
|
@@ -126,6 +128,14 @@ export declare class ServerAnalytics {
|
|
|
126
128
|
private tellKernelPid;
|
|
127
129
|
/** Kernel 0.4.0: counts this engine's own requests for the budget view (older kernels: nothing). */
|
|
128
130
|
private countBudget;
|
|
131
|
+
/** A player in this server's pid (free: their session), for the join row's ref / follow / party. */
|
|
132
|
+
private pidHere;
|
|
133
|
+
/**
|
|
134
|
+
* A player not in this server: this game's record of them (one GetAsync, no retries, only with read budget to
|
|
135
|
+
* spare). undefined when unknown (never seen by this game, no budget, or the store can't be read).
|
|
136
|
+
*/
|
|
137
|
+
private pidFromRecord;
|
|
138
|
+
/** The join row's join metadata (analytics/join.ts): GetJoinData, the player facts, the launch data's verdict. */
|
|
129
139
|
private joinSource;
|
|
130
140
|
/**
|
|
131
141
|
* How many of the player's friends are in this server: one `GetFriendsAsync` per join (at most FRIEND_PAGES_MAX
|