@typetorch/framework 0.6.2 → 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 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),
@@ -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:505]", `[analytics] {kind} event name must be 1-64 characters: {string.sub(tostring(name), 1, 80)}`)
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:557]", `[analytics] experiment("{string.sub(tostring(name), 1, 64)}"): bad name or variants (1-16 distinct names)`)
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:592]", `[analytics] the sink settings (settings.backend or settings.analytics): {errorsText}`)
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:619]", `[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 \})`)
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:1198]", `[analytics] client message failed: {string.sub(tostring(err), 1, 200)}`)
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:1393]", `[analytics] {message}{more}`)
2088
+ warn("[src/analytics/server.ts:1440]", `[analytics] {message}{more}`)
2038
2089
  return true
2039
2090
  end
2040
2091
  function ServerAnalytics:noteSuccess()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typetorch/framework",
3
- "version": "0.6.2",
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",