@indiegems/gem-web-sdk 7.4.0 → 7.4.2

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.
Files changed (38) hide show
  1. package/README.md +54 -0
  2. package/dist/gem-web-sdk.esm.js +1 -1
  3. package/dist/gem-web-sdk.game.js +1 -1
  4. package/dist/gem-web-sdk.min.js +1 -1
  5. package/dist/gem-web-sdk.sdk.js +1 -1
  6. package/dist/gem-web-sdk.server.js +1 -1
  7. package/docs/achievements.md +232 -0
  8. package/docs/analytics.md +271 -0
  9. package/docs/api/api-reference-game.md +15308 -0
  10. package/docs/api/api-reference-server.md +4397 -0
  11. package/docs/api/reference.json +24130 -0
  12. package/docs/arcade-economy.md +263 -0
  13. package/docs/async-opponents.md +396 -0
  14. package/docs/brand-art.md +334 -0
  15. package/docs/cross-promotion.md +137 -0
  16. package/docs/daily-challenges.md +583 -0
  17. package/docs/dedicated-server-flow.md +879 -0
  18. package/docs/deprecation-policy.md +124 -0
  19. package/docs/friends.md +233 -0
  20. package/docs/game-economy-tutorial.md +326 -0
  21. package/docs/getting-started.md +552 -0
  22. package/docs/inventory-and-catalogs.md +1035 -0
  23. package/docs/leaderboards.md +277 -0
  24. package/docs/manifest.json +135 -0
  25. package/docs/match-session.md +288 -0
  26. package/docs/p2p-and-parties.md +783 -0
  27. package/docs/platform-rules.md +372 -0
  28. package/docs/player-sign-in.md +89 -0
  29. package/docs/room-shapes.md +195 -0
  30. package/docs/rooms-and-matches.md +779 -0
  31. package/docs/saving-player-progress.md +457 -0
  32. package/docs/server-bundles.md +1041 -0
  33. package/docs/serverless-functions.md +748 -0
  34. package/docs/trading-and-gifting.md +98 -0
  35. package/docs/what-a-game-must-do.md +1231 -0
  36. package/package.json +7 -2
  37. package/schema/bridge.v1.json +5 -5
  38. package/schema/capabilities.public.json +189 -0
@@ -0,0 +1,271 @@
1
+ # Analytics
2
+
3
+ Your game does not have to measure anything. This is a capability, not an obligation — but if you
4
+ want to know how far players get into your tutorial, which difficulty they quit on, or whether the
5
+ level you rebalanced is actually finishing more often, the platform will count it for you and your
6
+ game reaches it with the token your frame already holds.
7
+
8
+ Two things decide whether this feature fits your game, and both are unusual enough to read before
9
+ you design around them:
10
+
11
+ - **Nothing is recorded that you did not declare.** There is no free-text property. Every event,
12
+ every dimension and every value a dimension may take is published in your game config ahead of
13
+ time, and anything else is refused per event rather than stored.
14
+ - **Detail older than 30 days is gone.** Raw events live 30 days. The daily counters built from
15
+ them live 400. A question you can only answer by reading individual events is a question you can
16
+ only ask about the last month.
17
+
18
+ What you record here is **yours**. It feeds your own reporting and nothing the platform treats as
19
+ authoritative — see [section 8](#8-what-analytics-is-not) before you attach a reward to it.
20
+
21
+ ## 1. Two roles, and they are not the same person
22
+
23
+ The same split as save slots and leaderboards: one half is something you publish, the other is
24
+ code your game runs.
25
+
26
+ | | Who | When |
27
+ | --------------------- | ------------------------ | ------------------------------------- |
28
+ | **Declaring events** | you, the developer | once, in your game config |
29
+ | **Recording events** | your game's running code | whenever the thing happens |
30
+
31
+ An event your config does not declare does not exist. Recording it is not an error your game can
32
+ catch — it is accepted into the queue, sent, and refused one event at a time when it arrives.
33
+ Section 7 is how you see that.
34
+
35
+ ## 2. Declare the events, in your game config
36
+
37
+ The `analytics_events` section names each event, the dimensions it is broken down by and the
38
+ numbers it carries:
39
+
40
+ ```yaml
41
+ analytics_events:
42
+ - id: level_completed
43
+ description: A level ended in a win or a loss.
44
+ dimensions:
45
+ outcome: { values: [win, loss, quit] }
46
+ difficulty: { values: [easy, normal, hard] }
47
+ level: { range: { min: 1, max: 50 } }
48
+ measures:
49
+ duration_ms: int
50
+ score: int
51
+ ```
52
+
53
+ **A dimension is a closed set, and that is the point.** Either a `values` list or a bounded
54
+ integer `range`, each at most 64 wide, at most three per event, and the product of an event's
55
+ dimension sizes is capped — the example above is 3 × 3 × 50. A string with no `values` list is a
56
+ config error rather than a warning, because the number of distinct rows an event can produce has
57
+ to be knowable before a player produces any of them.
58
+
59
+ **Measures are the numbers**, at most four per event, `int` or `float`. The platform keeps their
60
+ sum, count, minimum and maximum per day, and the individual value for as long as the raw window
61
+ holds it.
62
+
63
+ Ship it the way you ship any config:
64
+
65
+ ```bash
66
+ gemctl config schema # the authoritative field list
67
+ gemctl config upload --wait # register a version
68
+ gemctl config promote <version> --channel <channel> # this is what makes it live
69
+ ```
70
+
71
+ **Uploading is not shipping.** Until a version carrying `analytics_events` is promoted to the
72
+ channel your game runs on, every event your game records is refused `analytics_not_declared` —
73
+ including on a channel you use for development.
74
+
75
+ ## 3. Funnels are declared too
76
+
77
+ A funnel is an ordered list of two to eight event ids, declared once and read back as players at
78
+ each step:
79
+
80
+ ```yaml
81
+ analytics_funnels:
82
+ - id: first_session
83
+ steps: [gem_session_start, tutorial_completed, level_completed]
84
+ ```
85
+
86
+ Steps may name your own events or the platform's. **A funnel is answered two different ways and
87
+ the answer says which.** Inside the raw window it is ordered and can cross days, with the time
88
+ each player took to reach each step. Outside it, the only thing left is the daily counters, so the
89
+ answer becomes same-day and unordered: a player counts at a step if every earlier step has a row
90
+ on the same day. A window that straddles the boundary is answered entirely from the counters
91
+ rather than mixing the two definitions in one number.
92
+
93
+ **So do not compare a funnel reading from last week against one from last quarter.** They are
94
+ different questions with the same name.
95
+
96
+ ## 4. What the platform records for you
97
+
98
+ The SDK emits these on its own. You do not declare them, you cannot suppress them individually,
99
+ and a game that declares no events at all still gets them:
100
+
101
+ | Event | Dimensions | Measures |
102
+ | ---------------------------------- | -------------------------------- | ------------- |
103
+ | `gem_session_start` | — | — |
104
+ | `gem_session_end` | `reason` | `duration_ms` |
105
+ | `gem_pause` / `gem_resume` | — | — |
106
+ | `gem_boot_ready` | — | `boot_ms` |
107
+ | `gem_match_started` / `gem_match_ended` | `transport` (`dedicated`, `p2p`) | — |
108
+ | `gem_auth_lost` | — | — |
109
+
110
+ `reason` on a session end is one of `grant_expiring`, `session_ended`, `navigating`,
111
+ `kill_switch` or `pagehide`.
112
+
113
+ **The `gem_` prefix is reserved.** Your config cannot declare an id beginning with it and
114
+ `gem.analytics.track` refuses one rather than sending it, so pick another name.
115
+
116
+ **These columns are the whole of it, and `setContext` does not add to them.** The dimensions above
117
+ are fixed by the platform, and since no config can declare a `gem_` id there is no way to widen
118
+ one — a key that is not in this table would refuse the whole event rather than annotate it. Your
119
+ context therefore reaches your own `track` calls and stops there: these arrive carrying exactly
120
+ what the table says, however much context you have set.
121
+
122
+ Two of these are worth knowing the shape of before you build a report on them:
123
+
124
+ - **`gem_session_start` is the handshake, not the first frame.** A game opened outside the arcade
125
+ never establishes a play session, so it starts no session and sends nothing. That is expected
126
+ while you are developing locally.
127
+ - **`gem_session_end` on a closing page is best effort.** The browser gives a page that is going
128
+ away a small budget for requests in flight and the SDK spends part of it here; the room leave
129
+ gets the larger part, because a stranded room membership is the failure a player notices.
130
+ Treat `duration_ms` from a `pagehide` end as approximate.
131
+
132
+ `gem_match_ended` is skipped for a match this page never saw start — a reload into a running
133
+ match, for instance — because `transport` is a declared dimension and the SDK will not guess one.
134
+ **Match starts and match ends therefore do not reconcile exactly**; use starts for counting matches
135
+ and ends for breaking them down.
136
+
137
+ ## 5. Record an event
138
+
139
+ ```js
140
+ gem.analytics.track('level_completed', {
141
+ outcome: 'win', // a string is a dimension
142
+ difficulty: 'normal',
143
+ level: 7,
144
+ duration_ms: 48120, // a number is a measure
145
+ score: 3100,
146
+ });
147
+ ```
148
+
149
+ **A string is a dimension and a number is a measure**, and which one a key is was settled by your
150
+ config, not by the call. Passing `level: '7'` where the config declares a range is a refusal, not
151
+ a conversion.
152
+
153
+ `track` is synchronous, never throws and never rejects. It puts the event on an in-memory queue
154
+ that goes out on a timer, when it reaches 50 events, and on the way out of the page. **Nothing
155
+ is sent by the call itself**, so a game that never flushes still reports.
156
+
157
+ Values every event should carry go in the context instead of on every call site:
158
+
159
+ ```js
160
+ gem.analytics.setContext({ build: 'v1.4.2' });
161
+ ```
162
+
163
+ **Context reaches your events, never the platform's.** It is merged into what you pass `track` and
164
+ into nothing else — the `gem_*` events in section 4 have fixed dimensions no config can widen, so
165
+ a context key on one would refuse the event instead of labelling it. Setting a context cannot cost
166
+ you your session, pause, boot or match data.
167
+
168
+ **A context key is declared like any other.** One key your event does not carry refuses every
169
+ event it reaches, not just the one you were thinking about — which is why the SDK does not stamp
170
+ its own version or your game's onto your events. It offers them instead, for a game that has
171
+ declared the dimensions to take them:
172
+
173
+ ```js
174
+ gem.analytics.setContext(gem.analytics.environment);
175
+ ```
176
+
177
+ Every dimension you add costs cardinality against the product cap, so do that on the handful of
178
+ events where knowing which build produced a number is worth a dimension slot.
179
+
180
+ You can send the queue deliberately and read what happened to it:
181
+
182
+ ```js
183
+ const { accepted, rejected, dropped, duplicate } = await gem.analytics.flush();
184
+ ```
185
+
186
+ You rarely need to. The SDK flushes on its own every ten seconds — that is also the floor, because
187
+ the platform limits how often one player may post — and again on every way out of the page, so the
188
+ only reason to call it by hand is to look at the answer.
189
+
190
+ `GemOptions.analytics` carries the three knobs: `enabled`, `flushIntervalMs` and `maxQueue`.
191
+ **`enabled: false` turns your own `track` calls into no-ops and leaves the platform's `gem_*`
192
+ events alone** — it is narrower than it looks.
193
+
194
+ ## 6. What your dedicated server records, and why it differs
195
+
196
+ A server bundle reaches the same surface as `gem.analytics`, with one signature difference:
197
+
198
+ ```js
199
+ export default defineServer((gem) => ({
200
+ async shutdown() {
201
+ gem.analytics.track(winnerId, 'match_result', { outcome: 'win', score: 3100 });
202
+ await gem.analytics.flush();
203
+ },
204
+ }));
205
+ ```
206
+
207
+ **The player id comes first and is required.** A server credential names no player, so an event
208
+ from a server has to say who it is about and the platform will not infer one. Take the id from
209
+ `join()` or from the host's player list.
210
+
211
+ **Flush before your match ends.** A dedicated server is torn down when the match is over and
212
+ nothing gets a last word in for it — there is no closing page here to trigger a final send.
213
+ `await gem.analytics.flush()` in `shutdown` is the whole of it, and skipping it loses the tail of
214
+ the queue. See [server-bundles.md](server-bundles.md) for the rest of a bundle's lifecycle.
215
+
216
+ **A server cannot emit `gem_*` events.** Those are declared client-sourced, so a server-side
217
+ `gem_match_started` is refused; record your own event instead.
218
+
219
+ ## 7. When something is refused
220
+
221
+ **Refusals arrive inside a successful response, one per event.** A batch carrying one bad name is
222
+ not rejected — its other events are written and the bad one comes back at its index. The SDK
223
+ prints each refusal to the console in development, and `flush()` returns them in `rejected`.
224
+
225
+ | What you get | What happened | What to do |
226
+ | ------------------------------ | ------------------------------------------------------ | ----------------------------------------------------------------- |
227
+ | `analytics_not_declared` | This channel resolves no `analytics_events` at all | Promote a config version that has the section to this channel |
228
+ | `unknown_event` | The channel declares a set and your name is not in it | Check the id against your config — most often a typo or a new event not yet promoted |
229
+ | `unknown_dimension` | A key you sent is not on that event | Declare it, or stop sending it — context keys count |
230
+ | `missing_dimension` | A dimension the event declares was absent | Send every declared dimension on every event |
231
+ | `dimension_value_not_declared` | The value is outside the closed set | Add the value to the config, or map it to one that exists |
232
+ | `unknown_measure` | A number you sent is not a declared measure | Declare it, or drop it |
233
+ | `measure_wrong_type` | An `int` measure got a fraction, or a dimension got a number | Match the declared type — a string is a dimension, a number is a measure |
234
+ | `source_not_permitted` | This side may not emit this event | Narrow or widen `sources` on the event, or record it from the other side |
235
+
236
+ Two results are not refusals and should not be treated as one:
237
+
238
+ - **`dropped`** counts events the local queue threw away because it was full. The oldest go first
239
+ and the cap defaults to 500. It is nonzero only when your game produces events faster than they
240
+ can be sent, and it is worth alarming on.
241
+ - **`duplicate: true`** means the platform had already seen that batch and wrote nothing. That is
242
+ a retry whose first attempt in fact landed — it is what stops a send racing a page close from
243
+ being counted twice.
244
+
245
+ A rate limit or a temporarily unreachable backend **keeps the queue** and rejects the `flush`
246
+ promise, so the next send carries the same events. A refusal that would fail identically forever
247
+ discards its batch instead, because a queue that can never empty fills up and starts dropping the
248
+ events that would explain the problem.
249
+
250
+ ## 8. What analytics is not
251
+
252
+ **Client events are game-asserted browser facts.** Anything a browser can record, a modified
253
+ browser can record differently — the same boundary that keeps a leaderboard score off the client
254
+ in [leaderboards.md](leaderboards.md) and an economy grant behind a recipe in
255
+ [inventory-and-catalogs.md](inventory-and-catalogs.md).
256
+
257
+ So what you record here feeds **your** reporting and nothing else. It is not a basis for:
258
+
259
+ - **rewards or grants** — run those through the economy, where the arithmetic happens on the
260
+ server;
261
+ - **daily challenges or streaks** — those are config-declared and settled by the platform;
262
+ - **store ranking or discovery** — the platform does not read your events for any of it;
263
+ - **player progress the game reads back** — analytics is write-only from the game's side. Progress
264
+ that decides what a player sees belongs in [saving-player-progress.md](saving-player-progress.md).
265
+
266
+ When a fact has to be true rather than reported, record it from a dedicated server, which holds a
267
+ credential a player does not. That is the authoritative half of the pair, and it is the only half
268
+ that is.
269
+
270
+ **Nothing per-player comes back.** The reads answer with counts and with how many distinct players
271
+ reached a step; there is no drill-down to an individual, on any surface.