@typetorch/framework 0.6.1 → 0.6.3
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 +1 -1
- package/out/analytics/SCHEMA.md +2 -2
- package/out/analytics/server.d.ts +7 -0
- package/out/analytics/server.luau +57 -6
- package/out/devtools/client.luau +13 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -490,7 +490,7 @@ const variant = analytics.experiment("onboarding", ["short", "long"]); // same a
|
|
|
490
490
|
economy are server-authoritative): on the client they warn once and send nothing, and the server refuses them from
|
|
491
491
|
clients.
|
|
492
492
|
- **Options** (all on by default): `sessions` (joins, leaves, device, join source, account age bucket, Premium,
|
|
493
|
-
country, friends in the server, first-ever vs returning, days since the last visit), `tech` (FPS, ping, memory, load
|
|
493
|
+
country, the player policy, device trust, friends in the server, first-ever vs returning, days since the last visit), `tech` (FPS, ping, memory, load
|
|
494
494
|
time, client and server errors, swaps and rollbacks, leaves within 60 s of a swap), `zones` (parts or models tagged
|
|
495
495
|
`TTZone`, named by a `Name` attribute or the instance name), `screens` (ScreenGuis in PlayerGui, GuiObjects tagged
|
|
496
496
|
`TTScreen`), `recording` (the first-ever session in detail), `fleet` (kernel 0.3.2 heartbeats and deploy reports),
|
package/out/analytics/SCHEMA.md
CHANGED
|
@@ -57,12 +57,12 @@ 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), `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); `jd?` (plans/30, with the settings' `joinData` `"full"`, the default: the raw join data, below). With `joinData` `"off"` only `from`, `ctx`, `tp`, `party`, `age`, `prem`, `country`, `friends`, `ret`, `late` are written. `t` is the join time |
|
|
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); `pol` (the player policy, PolicyService:GetPolicyInfoForPlayerAsync with a 5 s deadline: `ok`, `failed`, `timeout`) and with `ok` `pol_ads`, `pol_rand` (ArePaidRandomItemsRestricted), `pol_trade` (IsPaidItemTradingAllowed), `pol_share` (IsContentSharingAllowed), `pol_subs` (IsEligibleToPurchaseSubscription), `pol_cn` (IsSubjectToChinaPolicies), `pol_links` (AllowedExternalLinkReferences, comma separated); `trust?` (Player.DeviceTrustLevel at the join, usually `Pending`: the leave row has the settled level); `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); `jd?` (plans/30, with the settings' `joinData` `"full"`, the default: the raw join data, below). With `joinData` `"off"` only `from`, `ctx`, `tp`, `party`, `age`, `prem`, `country`, `friends`, `ret`, `late` are written. `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
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
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
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"` |
|
|
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) |
|
|
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), `trust?` (Player.DeviceTrustLevel at the leave: `Pending` (never settled; always in Studio), `Unknown`, `None`, `Standard`, `Full`; missing on engines without it) |
|
|
66
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) |
|
|
67
67
|
| tech | `client` (src client) | Every `techEvery` s: `fps`, `mem` (MB, 10s), `ping?` (ms) |
|
|
68
68
|
| tech | `load` (src client) | `secs`: from the client starting to the engine starting (first client generation only) |
|
|
@@ -144,6 +144,13 @@ export declare class ServerAnalytics {
|
|
|
144
144
|
* stopped. No call when the player is alone; a failed call counts what it read (0 at first).
|
|
145
145
|
*/
|
|
146
146
|
private friendsHere;
|
|
147
|
+
/**
|
|
148
|
+
* The join's player policy (PolicyService:GetPolicyInfoForPlayerAsync): `pol` = `ok`, `failed` or `timeout` (no answer in
|
|
149
|
+
* POLICY_WAIT s), and with `ok` the answer: `pol_ads`, `pol_rand` (paid random items restricted), `pol_trade` (paid
|
|
150
|
+
* item trading allowed), `pol_share` (content sharing allowed), `pol_subs` (eligible to buy a subscription), `pol_cn`
|
|
151
|
+
* (China policies), `pol_links` (the allowed external link references, comma separated, "" = none).
|
|
152
|
+
*/
|
|
153
|
+
private policyProps;
|
|
147
154
|
private emitJoin;
|
|
148
155
|
private onPlayer;
|
|
149
156
|
private leave;
|
|
@@ -6,6 +6,7 @@ local DataStoreService = _services.DataStoreService
|
|
|
6
6
|
local HttpService = _services.HttpService
|
|
7
7
|
local LocalizationService = _services.LocalizationService
|
|
8
8
|
local Players = _services.Players
|
|
9
|
+
local PolicyService = _services.PolicyService
|
|
9
10
|
local RunService = _services.RunService
|
|
10
11
|
local Stats = _services.Stats
|
|
11
12
|
local Workspace = _services.Workspace
|
|
@@ -130,8 +131,22 @@ local RECORDING_MAX_CHUNKS = 80
|
|
|
130
131
|
local RECORDING_MAX_SECONDS = 900
|
|
131
132
|
--* The join event waits this long for the client's device info.
|
|
132
133
|
local DEVICE_WAIT = 8
|
|
134
|
+
--* The join event waits this long for PolicyService's answer (GetPolicyInfoForPlayerAsync has no timeout of its own).
|
|
135
|
+
local POLICY_WAIT = 5
|
|
133
136
|
local ZONE_INTERVAL = 0.5
|
|
134
137
|
local ZONE_TAG = "TTZone"
|
|
138
|
+
--[[
|
|
139
|
+
*
|
|
140
|
+
* Player.DeviceTrustLevel's name (Pending, Unknown, None, Standard, Full; Roblox sets it once, some time after the join,
|
|
141
|
+
* and Studio never does), or undefined on an engine without the property. Roblox acts on none of it: data only.
|
|
142
|
+
|
|
143
|
+
]]
|
|
144
|
+
local function deviceTrust(player)
|
|
145
|
+
local ok, level = pcall(function()
|
|
146
|
+
return player.DeviceTrustLevel
|
|
147
|
+
end)
|
|
148
|
+
return if ok and typeof(level) == "EnumItem" then level.Name else nil
|
|
149
|
+
end
|
|
135
150
|
--* One player's session. Plain data (persist), keyed by tostring(UserId).
|
|
136
151
|
local function nowMs()
|
|
137
152
|
return DateTime.now().UnixTimestampMillis
|
|
@@ -563,7 +578,7 @@ do
|
|
|
563
578
|
end
|
|
564
579
|
function ServerAnalytics:track(player, kind, name, props)
|
|
565
580
|
if not isEventName(name) then
|
|
566
|
-
warn("[src/analytics/server.ts:
|
|
581
|
+
warn("[src/analytics/server.ts:517]", `[analytics] {kind} event name must be 1-64 characters: {string.sub(tostring(name), 1, 80)}`)
|
|
567
582
|
return nil
|
|
568
583
|
end
|
|
569
584
|
local session = if player then self:sessionOf(player) else nil
|
|
@@ -671,7 +686,7 @@ do
|
|
|
671
686
|
end
|
|
672
687
|
local control = _condition
|
|
673
688
|
if not isExperimentName(name) or not isVariantList(variants) then
|
|
674
|
-
warn("[src/analytics/server.ts:
|
|
689
|
+
warn("[src/analytics/server.ts:569]", `[analytics] experiment("{string.sub(tostring(name), 1, 64)}"): bad name or variants (1-16 distinct names)`)
|
|
675
690
|
return control
|
|
676
691
|
end
|
|
677
692
|
local session = self:sessionOf(player)
|
|
@@ -736,7 +751,7 @@ do
|
|
|
736
751
|
function ServerAnalytics:applySettings(parsed)
|
|
737
752
|
local errorsText = table.concat(parsed.errors, "; ")
|
|
738
753
|
if errorsText ~= table.concat(self.settingsErrors, "; ") and errorsText ~= "" then
|
|
739
|
-
warn("[src/analytics/server.ts:
|
|
754
|
+
warn("[src/analytics/server.ts:604]", `[analytics] the sink settings (settings.backend or settings.analytics): {errorsText}`)
|
|
740
755
|
end
|
|
741
756
|
self.settingsErrors = parsed.errors
|
|
742
757
|
local hadSettings = self.settings ~= nil
|
|
@@ -766,7 +781,7 @@ do
|
|
|
766
781
|
-- `new AnalyticsEngine({ settings })`.
|
|
767
782
|
if not settingsSupported() then
|
|
768
783
|
if self.kernel ~= nil and self.kernel.test ~= true then
|
|
769
|
-
warn("[src/analytics/server.ts:
|
|
784
|
+
warn("[src/analytics/server.ts:631]", `[analytics] no sink settings: they come from the signed settings record (kernel 0.3.8+, "typetorch backend setup"); this server runs kernel {self.kernel.kernelVersion}. Or pass new AnalyticsEngine(\{ settings \})`)
|
|
770
785
|
end
|
|
771
786
|
self.settingsTried = true
|
|
772
787
|
return nil
|
|
@@ -1290,6 +1305,39 @@ do
|
|
|
1290
1305
|
end
|
|
1291
1306
|
return count
|
|
1292
1307
|
end
|
|
1308
|
+
function ServerAnalytics:policyProps(player, props)
|
|
1309
|
+
local info
|
|
1310
|
+
local failed = false
|
|
1311
|
+
task.spawn(function()
|
|
1312
|
+
local ok, result = pcall(function()
|
|
1313
|
+
return PolicyService:GetPolicyInfoForPlayerAsync(player)
|
|
1314
|
+
end)
|
|
1315
|
+
if ok and type(result) == "table" then
|
|
1316
|
+
info = result
|
|
1317
|
+
else
|
|
1318
|
+
failed = true
|
|
1319
|
+
end
|
|
1320
|
+
end)
|
|
1321
|
+
local deadline = os.clock() + POLICY_WAIT
|
|
1322
|
+
while info == nil and not failed and os.clock() < deadline and player.Parent ~= nil and not self.stopped do
|
|
1323
|
+
task.wait(0.1)
|
|
1324
|
+
end
|
|
1325
|
+
local answer = info
|
|
1326
|
+
if answer == nil then
|
|
1327
|
+
props.pol = if failed then "failed" else "timeout"
|
|
1328
|
+
return nil
|
|
1329
|
+
end
|
|
1330
|
+
props.pol = "ok"
|
|
1331
|
+
props.pol_ads = answer.AreAdsAllowed == true
|
|
1332
|
+
props.pol_rand = answer.ArePaidRandomItemsRestricted == true
|
|
1333
|
+
props.pol_trade = answer.IsPaidItemTradingAllowed == true
|
|
1334
|
+
props.pol_share = answer.IsContentSharingAllowed == true
|
|
1335
|
+
props.pol_subs = answer.IsEligibleToPurchaseSubscription == true
|
|
1336
|
+
props.pol_cn = answer.IsSubjectToChinaPolicies == true
|
|
1337
|
+
local _allowedExternalLinkReferences = answer.AllowedExternalLinkReferences
|
|
1338
|
+
local links = if type(_allowedExternalLinkReferences) == "table" then (answer.AllowedExternalLinkReferences) else {}
|
|
1339
|
+
props.pol_links = string.sub(table.concat(links, ","), 1, 120)
|
|
1340
|
+
end
|
|
1293
1341
|
function ServerAnalytics:emitJoin(player, session)
|
|
1294
1342
|
local deadline = os.clock() + DEVICE_WAIT
|
|
1295
1343
|
while session.device == nil and os.clock() < deadline and player.Parent ~= nil and not self.stopped do
|
|
@@ -1310,6 +1358,8 @@ do
|
|
|
1310
1358
|
if countryOk then
|
|
1311
1359
|
props.country = country
|
|
1312
1360
|
end
|
|
1361
|
+
self:policyProps(player, props)
|
|
1362
|
+
props.trust = deviceTrust(player)
|
|
1313
1363
|
props.friends = self:friendsHere(player)
|
|
1314
1364
|
props.ret = session.ret
|
|
1315
1365
|
-- The engine started after this player joined (created later, or added by a deploy).
|
|
@@ -1383,6 +1433,7 @@ do
|
|
|
1383
1433
|
self:emit(session, "session", "leave", encodeProps({
|
|
1384
1434
|
secs = math.floor((now - session.joinedAt) / 1000),
|
|
1385
1435
|
why = why,
|
|
1436
|
+
trust = deviceTrust(player),
|
|
1386
1437
|
swap60 = if lastSwap ~= nil and os.time() - lastSwap <= 60 then true else nil,
|
|
1387
1438
|
}))
|
|
1388
1439
|
end
|
|
@@ -1751,7 +1802,7 @@ do
|
|
|
1751
1802
|
end
|
|
1752
1803
|
end)
|
|
1753
1804
|
if not ok then
|
|
1754
|
-
warn("[src/analytics/server.ts:
|
|
1805
|
+
warn("[src/analytics/server.ts:1245]", `[analytics] client message failed: {string.sub(tostring(err), 1, 200)}`)
|
|
1755
1806
|
end
|
|
1756
1807
|
end)
|
|
1757
1808
|
self.trove:add(function()
|
|
@@ -2034,7 +2085,7 @@ do
|
|
|
2034
2085
|
self.lastWarnAt = now
|
|
2035
2086
|
local more = if self.suppressed > 0 then ` (+{self.suppressed} more in the last minute)` else ""
|
|
2036
2087
|
self.suppressed = 0
|
|
2037
|
-
warn("[src/analytics/server.ts:
|
|
2088
|
+
warn("[src/analytics/server.ts:1440]", `[analytics] {message}{more}`)
|
|
2038
2089
|
return true
|
|
2039
2090
|
end
|
|
2040
2091
|
function ServerAnalytics:noteSuccess()
|
package/out/devtools/client.luau
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
-- Compiled with roblox-ts v3.0.0
|
|
2
2
|
local TS = _G[script]
|
|
3
3
|
local _services = TS.import(script, TS.getModule(script, "@rbxts", "services"))
|
|
4
|
+
local GuiService = _services.GuiService
|
|
4
5
|
local HttpService = _services.HttpService
|
|
5
6
|
local Players = _services.Players
|
|
6
7
|
local ReplicatedStorage = _services.ReplicatedStorage
|
|
@@ -2654,8 +2655,17 @@ local function startDevtoolsClient(kernel, dispatcher, trove)
|
|
|
2654
2655
|
toggle.TextXAlignment = Enum.TextXAlignment.Center
|
|
2655
2656
|
toggle.TextColor3 = COLORS.dark
|
|
2656
2657
|
toggle.BackgroundColor3 = COLORS.accent
|
|
2657
|
-
toggle.Size = UDim2.fromOffset(
|
|
2658
|
-
|
|
2658
|
+
toggle.Size = UDim2.fromOffset(52, 32)
|
|
2659
|
+
-- In Roblox's top bar row, right of its own buttons (GuiService.TopbarInset: the free part of the bar), so it never covers a
|
|
2660
|
+
-- game's top-left HUD (2026-11-07: on a phone the old fixed 12, 70 sat on the game's cash chip). No inset (an old top bar or none):
|
|
2661
|
+
-- the old spot.
|
|
2662
|
+
local placeToggle = function()
|
|
2663
|
+
local inset = GuiService.TopbarInset
|
|
2664
|
+
local height = inset.Max.Y - inset.Min.Y
|
|
2665
|
+
toggle.Position = if inset.Width > 0 and height > 0 then UDim2.fromOffset(math.floor(inset.Min.X + 8), math.floor(inset.Min.Y + (height - 32) / 2)) else UDim2.fromOffset(12, 70)
|
|
2666
|
+
end
|
|
2667
|
+
placeToggle()
|
|
2668
|
+
trove:add(GuiService:GetPropertyChangedSignal("TopbarInset"):Connect(placeToggle))
|
|
2659
2669
|
-- Above every window (their ZIndex is their stacking order), under the page picker and the copy popup.
|
|
2660
2670
|
toggle.ZIndex = 800
|
|
2661
2671
|
corner(toggle, 8)
|
|
@@ -3259,7 +3269,7 @@ local function startDevtoolsClient(kernel, dispatcher, trove)
|
|
|
3259
3269
|
return playReloadSound()
|
|
3260
3270
|
end)
|
|
3261
3271
|
if not ok then
|
|
3262
|
-
warn("[src/devtools/client.ts:
|
|
3272
|
+
warn("[src/devtools/client.ts:2280]", `[devtools] reload sound failed: {err}`)
|
|
3263
3273
|
end
|
|
3264
3274
|
end))
|
|
3265
3275
|
end,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typetorch/framework",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "TypeTorch framework for roblox-ts: hot-swappable modules (dependency injection, lifecycle, troves), guarded networking, hot assets and the in-game dev menu. Ships inside every TypeTorch artifact.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "TypeTorch",
|