2.0.0: changelog and skill docs for slugs and environments
CI / check (push) Successful in 16s
CI / publish (push) Has been skipped

Claude-Session: https://claude.ai/code/session_01SMCvdwDmuxoaqGgvGBLk1V
This commit is contained in:
edmand46
2026-09-06 22:39:03 +03:00
parent a7401b534a
commit 7271874cac
9 changed files with 76 additions and 28 deletions
+33
View File
@@ -1,5 +1,38 @@
# Changelog
## 2.0.0
Breaking change — major bump, required by the backend environments release.
Every project now has exactly two environments, `staging` and `prod`, and the
SDK key passed as `projectKey` decides which one the player belongs to. The
environment never appears in the client API: it is resolved at login and
carried inside the access and refresh tokens. Tokens issued before this
release have no environment claim and are rejected with 401, so the first call
after the backend upgrade refreshes, fails, clears the token store and emits
`'signed-out'` to every `onAuthStateChange` listener. Log the player in again
with `client.auth.loginWithDevice()` / `loginWithCustom()`.
Quests, scenarios and offers are addressed by their slug instead of their id,
because ids differ between staging and prod while slugs are stable. Renamed
accordingly: `Quest.id` is now `Quest.slug`, `client.quests.claim()` takes a
quest slug, `reportProgress()` resolves to the slugs of the completed quests
(`ReportQuestProgressResponse.completedQuestSlugs`),
`client.stores.purchase(storeSlug, offerSlug, options?)` takes an offer slug
and `OfferHandle` carries a `slug` alongside its `id`,
`QuestMetrics.purchaseOffer(offerSlug)` builds its metric from the offer slug,
and every scenario-scoped type exposes `scenarioSlug` instead of `scenarioId`
`PendingEffect`, `ScenarioCompletedEffect`, `ScenarioFailedEffect`, the battle
pass requests and `client.battlePass.getProgress(scenarioSlug, nodeId)`.
`offer.buy()` is unchanged; it now binds the slug for you. Leaderboards, items
and stores already used slugs and are untouched.
Also shipped here, previously committed but never published: the effects client
reconciles against every `GET /sdk/v1/scenarios/pending` response, so a run
that disappears server-side (finished elsewhere, expired after a promote) now
emits `onScenarioCompleted` instead of lingering; and `run_not_active` joins
`unknown_run` and `run_expired` as a terminal rejection that drops the run and
emits `onScenarioFailed`.
## 1.0.0
- Scenario execution moved server-side. The SDK no longer walks a local DAG
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@rudder/js-sdk",
"version": "1.0.0",
"version": "2.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@rudder/js-sdk",
"version": "1.0.0",
"version": "2.0.0",
"devDependencies": {
"@types/node": "^26.1.1",
"jsdom": "^29.1.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@rudder/js-sdk",
"version": "1.0.0",
"version": "2.0.0",
"publishConfig": {
"registry": "https://hub.rudder.build/api/packages/rudder/npm/"
},
+1 -1
View File
@@ -74,7 +74,7 @@ service, or standalone economy service in this SDK — wallets live on
| Player profile + wallets | `client.player` | observable `SyncedState<PlayerProfile>` |
| Inventory (catalog-merged) | `client.inventory` | observable `SyncedState<InventoryItem[]>` |
| Catalog | `client.catalog` | observable `SyncedState<Map<string, CatalogItem>>` |
| Stores / purchases | `client.stores` + `client.stores.purchase(slug, offerId, opts?)` | observable `SyncedState<ShopHandle[]>` |
| Stores / purchases | `client.stores` + `client.stores.purchase(slug, offerSlug, opts?)` | observable `SyncedState<ShopHandle[]>` |
| Remote config | `client.remoteConfig` + `.get(key, default?)` | observable, typed |
| Player storage | `client.storage` + `.save(items)` / `.delete(type)` | observable + mutations |
| Project storage | `client.projectStorage` + `.save(items)` | observable + mutation |
+13
View File
@@ -50,6 +50,19 @@ On success both tokens are saved to the client's `TokenStore`, the runtime
starts (domains warmed, scenario engine restored, `player_login` event fired),
and the state flips to `'signed-in'`.
## Environments
A project has two environments, `staging` and `prod`. The SDK key you pass as
`RudderClientOptions.projectKey` belongs to one of them, so the environment is
resolved at login and carried inside the access and refresh tokens; nothing in
the client API takes an environment argument, and a player created in one
environment is invisible in the other. Content released only to `staging` is
empty for a `prod` key and vice versa.
Tokens issued before SDK 2.0.0 carry no environment claim and are rejected with
401. The transport's refresh then fails, clears the token store and emits
`'signed-out'` — log the player in again.
## Auth state
```ts
@@ -2,7 +2,7 @@
`BattlePassService` (source: `src/battlepass/BattlePassService.ts`).
Call-and-response access to battle pass endpoints. Battle pass state is tied to
a **scenario battle pass node**, so calls carry `scenarioId` + `nodeId` (plus
a **scenario battle pass node**, so calls carry `scenarioSlug` + `nodeId` (plus
`runId` for mutating calls). NOT observable — re-fetch progress explicitly
after a mutation.
@@ -14,7 +14,7 @@ you already know the scenario/node/run identifiers.
## Methods
```ts
getProgress(scenarioId: string, nodeId: string): Promise<GetBattlePassProgressResponse>
getProgress(scenarioSlug: string, nodeId: string): Promise<GetBattlePassProgressResponse>
addXp(request: AddBattlePassXpRequest): Promise<AddBattlePassXpResponse>
claimReward(request: ClaimBattlePassRewardRequest): Promise<ClaimBattlePassRewardResponse>
purchasePremium(request: PurchaseBattlePassPremiumRequest): Promise<PurchaseBattlePassPremiumResponse>
@@ -27,7 +27,7 @@ interface AddBattlePassXpRequest {
amount?: number;
nodeId?: string;
runId?: string;
scenarioId?: string;
scenarioSlug?: string;
source?: string; // configured XP source
}
interface AddBattlePassXpResponse {
@@ -41,7 +41,7 @@ interface ClaimBattlePassRewardRequest {
level?: number;
nodeId?: string;
runId?: string;
scenarioId?: string;
scenarioSlug?: string;
track?: 'free' | 'premium';
}
interface ClaimBattlePassRewardResponse {
@@ -62,7 +62,7 @@ interface PurchaseBattlePassPremiumRequest {
idempotencyKey?: string;
nodeId?: string;
runId?: string;
scenarioId?: string;
scenarioSlug?: string;
}
interface PurchaseBattlePassPremiumResponse {
error?: string;
@@ -80,5 +80,5 @@ interface PurchaseBattlePassPremiumResponse {
in the response and the typed error `code` (`RudderErrorCodes`).
- `purchasePremium` charges the player's wallet; idempotent; pass your own
`idempotencyKey` for safe retries.
- Prefer the `onBattlePass` effect session, which binds `scenarioId` /
- Prefer the `onBattlePass` effect session, which binds `scenarioSlug` /
`nodeId` / `runId` and posts scenario callbacks for you.
+6 -6
View File
@@ -11,15 +11,15 @@ Distinct from scenario quest nodes, which advance through
```ts
list(): Promise<Quest[]>
claim(questId: string): Promise<ClaimQuestResponse>
reportProgress(metric: string, amount: number): Promise<string[]> // ids of quests completed by this report
claim(questSlug: string): Promise<ClaimQuestResponse>
reportProgress(metric: string, amount: number): Promise<string[]> // slugs of quests completed by this report
```
## Types
```ts
interface Quest {
id?: string;
slug?: string; // stable across environments — what claim() takes
name?: string;
objectives?: QuestObjectiveProgress[];
rewards?: Reward[];
@@ -47,12 +47,12 @@ interface ClaimQuestResponse {
```ts
const quests = await client.quests.list();
for (const quest of quests) {
if (quest.status === 'completed' && quest.id) {
const res = await client.quests.claim(quest.id);
if (quest.status === 'completed' && quest.slug) {
const res = await client.quests.claim(quest.slug);
// res.success / res.alreadyClaimed / res.granted
}
}
const completedIds = await client.quests.reportProgress('kills', 1);
const completedSlugs = await client.quests.reportProgress('kills', 1);
```
- `claim` is idempotent server-side; check `success` / `alreadyClaimed` /
+4 -4
View File
@@ -122,7 +122,7 @@ counter response may carry the next `PendingEffect`.
}
```
These wrap `client.battlePass` with the run's `scenarioId`/`nodeId`/`runId`
These wrap `client.battlePass` with the run's `scenarioSlug`/`nodeId`/`runId`
already bound — prefer them over calling the service manually.
### `BattlePassLevelEffect` — a `battlepass_level` node (single claimable tier)
@@ -134,9 +134,9 @@ already bound — prefer them over calling the service manually.
### Run lifecycle effects
```ts
interface ScenarioCompletedEffect { readonly runId: string; readonly scenarioId: string }
interface ScenarioCompletedEffect { readonly runId: string; readonly scenarioSlug: string }
interface ScenarioFailedEffect {
readonly runId: string; readonly scenarioId: string; readonly nodeId: string;
readonly runId: string; readonly scenarioSlug: string; readonly nodeId: string;
readonly error: Error;
}
```
@@ -154,7 +154,7 @@ not delivered to the client.
## Reliability notes
- Completion methods POST `/sdk/v1/scenarios/callback` with
`{scenarioId, runId, nodeId, handle}` using the handles `output`,
`{scenarioSlug, runId, nodeId, handle}` using the handles `output`,
`onPurchase`, `onDecline`, `onEnd`, `onClaim`, `onComplete`, `onLevelUp`,
`onPremiumPurchase`.
- Transient callback failures (network error, 5xx) leave the effect active
+10 -8
View File
@@ -13,7 +13,7 @@ client.stores.onChange(cb);
await client.stores.load() / .reload();
// Direct purchase (bypassing handles):
await client.stores.purchase(storeSlug, offerId, options?);
await client.stores.purchase(storeSlug, offerSlug, options?);
```
## Handles
@@ -28,7 +28,8 @@ class ShopHandle {
}
class OfferHandle {
readonly id: string;
readonly id: string; // authoring id, differs between staging and prod
readonly slug: string; // stable across environments — what purchases use
readonly name?: string;
readonly price?: OfferPrice; // { amount?: number; currency?: string }
readonly contents: OfferContent[]; // { amount?: number; itemId?: string }[]
@@ -48,15 +49,15 @@ interface PurchaseOfferResponse {
## Purchase semantics
- `stores.purchase(storeSlug, offerId, options?)` and `offer.buy(options?)` are
the same executor; handles just bind the slug/id.
- `stores.purchase(storeSlug, offerSlug, options?)` and `offer.buy(options?)` are
the same executor; handles just bind the store slug and the offer slug.
- When `options.idempotencyKey` is omitted the SDK generates
`crypto.randomUUID()` per call — safe retries require passing your own key.
- On success the executor invalidates `player`, `inventory`, and `stores`, so
subscribers observe fresh wallet/inventory/store data immediately.
- Check `response.success` / `response.error` — a failed purchase is a resolved
response, not necessarily a thrown error.
- Purchase metrics (`purchase.offer:<offerId>`, `purchase.item:<itemId>`) are
- Purchase metrics (`purchase.offer:<offerSlug>`, `purchase.item:<itemId>`) are
reported to quests automatically server-side — do not report them manually
(see `reference/quests.md`).
@@ -71,10 +72,11 @@ interface Store {
}
interface Offer {
contents?: OfferContent[]; createdAt?: string; id?: string;
maxPurchases?: number; name?: string; price?: OfferPrice; updatedAt?: string;
maxPurchases?: number; name?: string; price?: OfferPrice; slug?: string;
updatedAt?: string;
}
```
`ShopHandle`/`OfferHandle` throw a plain `Error` if constructed from a store
without `slug` / an offer without `id` — in practice the domain only builds
handles from server data that has both.
without `slug` / an offer without `id` or `slug` — in practice the domain only
builds handles from server data that has all of them.