Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 372ab7cf7e | |||
| 62025592ba |
@@ -14,8 +14,8 @@ public class AddBattlePassXpRequest
|
||||
[JsonProperty("runId")]
|
||||
public string? RunId { get; set; }
|
||||
|
||||
[JsonProperty("scenarioId")]
|
||||
public string? ScenarioId { get; set; }
|
||||
[JsonProperty("scenarioSlug")]
|
||||
public string? ScenarioSlug { get; set; }
|
||||
|
||||
[JsonProperty("source")]
|
||||
public string? Source { get; set; }
|
||||
|
||||
@@ -14,8 +14,8 @@ public class ClaimBattlePassRewardRequest
|
||||
[JsonProperty("runId")]
|
||||
public string? RunId { get; set; }
|
||||
|
||||
[JsonProperty("scenarioId")]
|
||||
public string? ScenarioId { get; set; }
|
||||
[JsonProperty("scenarioSlug")]
|
||||
public string? ScenarioSlug { get; set; }
|
||||
|
||||
[JsonProperty("track")]
|
||||
public string? Track { get; set; }
|
||||
|
||||
@@ -8,7 +8,7 @@ public class GetBattlePassProgressRequest
|
||||
[JsonProperty("nodeId")]
|
||||
public string? NodeId { get; set; }
|
||||
|
||||
[JsonProperty("scenarioId")]
|
||||
public string? ScenarioId { get; set; }
|
||||
[JsonProperty("scenarioSlug")]
|
||||
public string? ScenarioSlug { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ public class PurchaseBattlePassPremiumRequest
|
||||
[JsonProperty("runId")]
|
||||
public string? RunId { get; set; }
|
||||
|
||||
[JsonProperty("scenarioId")]
|
||||
public string? ScenarioId { get; set; }
|
||||
[JsonProperty("scenarioSlug")]
|
||||
public string? ScenarioSlug { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,36 @@
|
||||
# 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 you configure 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 stored tokens and raises
|
||||
`AuthService.AuthStateChanged` with `RudderAuthState.SignedOut`. Log the player
|
||||
in again with `Auth.LoginWithDeviceAsync` / `LoginWithCustomAsync`.
|
||||
|
||||
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`, `QuestsService.ClaimAsync` takes a
|
||||
quest slug, `ReportProgressAsync` returns the slugs of the completed quests
|
||||
(`ReportQuestProgressResponse.CompletedQuestSlugs`), `StoresService.PurchaseAsync`
|
||||
takes an offer slug and `Offer` carries a `Slug`, `QuestMetrics.PurchaseOffer`
|
||||
builds its metric from the offer slug, and every scenario-scoped type exposes
|
||||
`ScenarioSlug` instead of `ScenarioId` — `PendingEffect`, the effect objects,
|
||||
`ScenarioCompletedEffect`, `ScenarioFailedEffect`, the battle pass requests and
|
||||
`BattlePassService.GetProgressAsync`. Leaderboards, items and stores already
|
||||
used slugs and are unchanged.
|
||||
|
||||
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
|
||||
|
||||
Breaking change — major bump. The local scenario engine is replaced by a
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace RudderSdk.Core.Models.Quests;
|
||||
|
||||
public class ClaimQuestRequest
|
||||
{
|
||||
[JsonProperty("questId")]
|
||||
public string? QuestId { get; set; }
|
||||
[JsonProperty("questSlug")]
|
||||
public string? QuestSlug { get; set; }
|
||||
|
||||
}
|
||||
|
||||
+3
-3
@@ -7,9 +7,6 @@ namespace RudderSdk.Core.Models.Quests;
|
||||
|
||||
public class Quest
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public string? Id { get; set; }
|
||||
|
||||
[JsonProperty("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
@@ -19,6 +16,9 @@ public class Quest
|
||||
[JsonProperty("rewards")]
|
||||
public List<Reward>? Rewards { get; set; }
|
||||
|
||||
[JsonProperty("slug")]
|
||||
public string? Slug { get; set; }
|
||||
|
||||
[JsonProperty("status")]
|
||||
public string? Status { get; set; }
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace RudderSdk.Core.Models.Quests;
|
||||
|
||||
public class ReportQuestProgressResponse
|
||||
{
|
||||
[JsonProperty("completedQuestIds")]
|
||||
public List<string>? CompletedQuestIds { get; set; }
|
||||
[JsonProperty("completedQuestSlugs")]
|
||||
public List<string>? CompletedQuestSlugs { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ var quests = await client.Quests.ListAsync();
|
||||
foreach (var quest in quests)
|
||||
{
|
||||
if (quest.Status == "completed")
|
||||
await client.Quests.ClaimAsync(quest.Id);
|
||||
await client.Quests.ClaimAsync(quest.Slug);
|
||||
}
|
||||
|
||||
// Custom metrics advance matching objectives server-side; the call returns
|
||||
@@ -82,8 +82,8 @@ var completedIds = await client.Quests.ReportProgressAsync("kills", 1);
|
||||
```
|
||||
|
||||
Purchase metrics are reported automatically by store purchases;
|
||||
`QuestMetrics.PurchaseOffer(offerId)` / `QuestMetrics.PurchaseItem(itemId)`
|
||||
name the format (`purchase.offer:<offerId>`, `purchase.item:<itemId>`) so
|
||||
`QuestMetrics.PurchaseOffer(offerSlug)` / `QuestMetrics.PurchaseItem(itemId)`
|
||||
name the format (`purchase.offer:<offerSlug>`, `purchase.item:<itemId>`) so
|
||||
quest configs and client code agree on it.
|
||||
|
||||
## Sessions
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
<AssemblyName>Rudder.Core</AssemblyName>
|
||||
<RootNamespace>RudderSdk.Core</RootNamespace>
|
||||
<PackageId>Rudder.Core</PackageId>
|
||||
<Version>1.0.0</Version>
|
||||
<Version>2.0.0</Version>
|
||||
<Authors>Rudder</Authors>
|
||||
<Description>Rudder LiveOps client SDK for .NET: auth, player, stores, battle pass, quests, leaderboards, inventory, remote config, scenarios and storage.</Description>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
|
||||
@@ -14,7 +14,7 @@ public class HandleScenarioCallbackRequest
|
||||
[JsonProperty("runId")]
|
||||
public string? RunId { get; set; }
|
||||
|
||||
[JsonProperty("scenarioId")]
|
||||
public string? ScenarioId { get; set; }
|
||||
[JsonProperty("scenarioSlug")]
|
||||
public string? ScenarioSlug { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ public class PendingEffect
|
||||
[JsonProperty("runId")]
|
||||
public string RunId { get; set; } = null!;
|
||||
|
||||
[JsonProperty("scenarioId")]
|
||||
public string ScenarioId { get; set; } = null!;
|
||||
[JsonProperty("scenarioSlug")]
|
||||
public string ScenarioSlug { get; set; } = null!;
|
||||
|
||||
[JsonProperty("type")]
|
||||
public string Type { get; set; } = null!;
|
||||
|
||||
@@ -17,7 +17,7 @@ public class UpdateScenarioCounterRequest
|
||||
[JsonProperty("runId")]
|
||||
public string? RunId { get; set; }
|
||||
|
||||
[JsonProperty("scenarioId")]
|
||||
public string? ScenarioId { get; set; }
|
||||
[JsonProperty("scenarioSlug")]
|
||||
public string? ScenarioSlug { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -24,11 +24,11 @@ public sealed class BattlePassService
|
||||
internal BattlePassService(RudderClient client) => _client = client;
|
||||
|
||||
/// <summary>Reads current progress: xp, level, premium ownership, claimed tiers.</summary>
|
||||
public Task<GetBattlePassProgressResponse> GetProgressAsync(string scenarioId, string nodeId, CancellationToken cancellationToken = default)
|
||||
public Task<GetBattlePassProgressResponse> GetProgressAsync(string scenarioSlug, string nodeId, CancellationToken cancellationToken = default)
|
||||
=> _client.SendAsync<GetBattlePassProgressRequest, GetBattlePassProgressResponse>(
|
||||
"POST",
|
||||
"/sdk/v1/battlepass/progress",
|
||||
new GetBattlePassProgressRequest { ScenarioId = scenarioId, NodeId = nodeId },
|
||||
new GetBattlePassProgressRequest { ScenarioSlug = scenarioSlug, NodeId = nodeId },
|
||||
cancellationToken);
|
||||
|
||||
/// <summary>Credits xp and returns the new xp/level and level-up flags.</summary>
|
||||
|
||||
@@ -26,7 +26,7 @@ public sealed class BattlePassEffect
|
||||
public string RunId => _handle.RunId;
|
||||
|
||||
/// <summary>Scenario id.</summary>
|
||||
public string ScenarioId => _handle.ScenarioId;
|
||||
public string ScenarioSlug => _handle.ScenarioSlug;
|
||||
|
||||
/// <summary>Node id.</summary>
|
||||
public string NodeId => _handle.NodeId;
|
||||
@@ -39,13 +39,13 @@ public sealed class BattlePassEffect
|
||||
|
||||
/// <summary>Reads current progress (xp, level, premium ownership, claimed tiers) for this node.</summary>
|
||||
public Task<GetBattlePassProgressResponse> GetProgressAsync(CancellationToken cancellationToken = default)
|
||||
=> _battlePass.GetProgressAsync(_handle.ScenarioId, _handle.NodeId, cancellationToken);
|
||||
=> _battlePass.GetProgressAsync(_handle.ScenarioSlug, _handle.NodeId, cancellationToken);
|
||||
|
||||
/// <summary>Credits xp from a configured source.</summary>
|
||||
public Task<AddBattlePassXpResponse> AddXpAsync(string source, long amount, CancellationToken cancellationToken = default)
|
||||
=> _battlePass.AddXpAsync(new AddBattlePassXpRequest
|
||||
{
|
||||
ScenarioId = _handle.ScenarioId,
|
||||
ScenarioSlug = _handle.ScenarioSlug,
|
||||
NodeId = _handle.NodeId,
|
||||
RunId = _handle.RunId,
|
||||
Source = source,
|
||||
@@ -59,7 +59,7 @@ public sealed class BattlePassEffect
|
||||
public Task<ClaimBattlePassRewardResponse> ClaimRewardAsync(int level, string track, CancellationToken cancellationToken = default)
|
||||
=> _battlePass.ClaimRewardAsync(new ClaimBattlePassRewardRequest
|
||||
{
|
||||
ScenarioId = _handle.ScenarioId,
|
||||
ScenarioSlug = _handle.ScenarioSlug,
|
||||
NodeId = _handle.NodeId,
|
||||
RunId = _handle.RunId,
|
||||
Level = level,
|
||||
@@ -71,7 +71,7 @@ public sealed class BattlePassEffect
|
||||
{
|
||||
var response = await _battlePass.PurchasePremiumAsync(new PurchaseBattlePassPremiumRequest
|
||||
{
|
||||
ScenarioId = _handle.ScenarioId,
|
||||
ScenarioSlug = _handle.ScenarioSlug,
|
||||
NodeId = _handle.NodeId,
|
||||
RunId = _handle.RunId,
|
||||
IdempotencyKey = Guid.NewGuid().ToString()
|
||||
|
||||
@@ -19,7 +19,7 @@ public sealed class BattlePassLevelEffect
|
||||
public string RunId => _handle.RunId;
|
||||
|
||||
/// <summary>Scenario id.</summary>
|
||||
public string ScenarioId => _handle.ScenarioId;
|
||||
public string ScenarioSlug => _handle.ScenarioSlug;
|
||||
|
||||
/// <summary>Node id.</summary>
|
||||
public string NodeId => _handle.NodeId;
|
||||
|
||||
@@ -15,7 +15,7 @@ public sealed class LeaderboardEffect
|
||||
public string RunId => _handle.RunId;
|
||||
|
||||
/// <summary>Scenario id.</summary>
|
||||
public string ScenarioId => _handle.ScenarioId;
|
||||
public string ScenarioSlug => _handle.ScenarioSlug;
|
||||
|
||||
/// <summary>Node id.</summary>
|
||||
public string NodeId => _handle.NodeId;
|
||||
|
||||
@@ -15,7 +15,7 @@ public sealed class NotificationEffect
|
||||
public string RunId => _handle.RunId;
|
||||
|
||||
/// <summary>Scenario id.</summary>
|
||||
public string ScenarioId => _handle.ScenarioId;
|
||||
public string ScenarioSlug => _handle.ScenarioSlug;
|
||||
|
||||
/// <summary>Node id.</summary>
|
||||
public string NodeId => _handle.NodeId;
|
||||
|
||||
@@ -20,7 +20,7 @@ public sealed class QuestEffect
|
||||
public string RunId => _handle.RunId;
|
||||
|
||||
/// <summary>Scenario id.</summary>
|
||||
public string ScenarioId => _handle.ScenarioId;
|
||||
public string ScenarioSlug => _handle.ScenarioSlug;
|
||||
|
||||
/// <summary>Node id.</summary>
|
||||
public string NodeId => _handle.NodeId;
|
||||
|
||||
@@ -3,15 +3,15 @@ namespace RudderSdk.Core;
|
||||
/// <summary>Payload of <see cref="EffectsService.OnScenarioCompleted"/>.</summary>
|
||||
public sealed class ScenarioCompletedEffect
|
||||
{
|
||||
internal ScenarioCompletedEffect(string runId, string scenarioId)
|
||||
internal ScenarioCompletedEffect(string runId, string scenarioSlug)
|
||||
{
|
||||
RunId = runId;
|
||||
ScenarioId = scenarioId;
|
||||
ScenarioSlug = scenarioSlug;
|
||||
}
|
||||
|
||||
/// <summary>Server-issued run id.</summary>
|
||||
public string RunId { get; }
|
||||
|
||||
/// <summary>Scenario id.</summary>
|
||||
public string ScenarioId { get; }
|
||||
public string ScenarioSlug { get; }
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ namespace RudderSdk.Core;
|
||||
/// <summary>Payload of <see cref="EffectsService.OnScenarioFailed"/>.</summary>
|
||||
public sealed class ScenarioFailedEffect
|
||||
{
|
||||
internal ScenarioFailedEffect(string runId, string scenarioId, string nodeId, Exception exception)
|
||||
internal ScenarioFailedEffect(string runId, string scenarioSlug, string nodeId, Exception exception)
|
||||
{
|
||||
RunId = runId;
|
||||
ScenarioId = scenarioId;
|
||||
ScenarioSlug = scenarioSlug;
|
||||
NodeId = nodeId;
|
||||
Exception = exception;
|
||||
}
|
||||
@@ -17,7 +17,7 @@ public sealed class ScenarioFailedEffect
|
||||
public string RunId { get; }
|
||||
|
||||
/// <summary>Scenario id.</summary>
|
||||
public string ScenarioId { get; }
|
||||
public string ScenarioSlug { get; }
|
||||
|
||||
/// <summary>Node the failure happened at.</summary>
|
||||
public string NodeId { get; }
|
||||
|
||||
@@ -15,7 +15,7 @@ public sealed class StoreOfferEffect
|
||||
public string RunId => _handle.RunId;
|
||||
|
||||
/// <summary>Scenario id.</summary>
|
||||
public string ScenarioId => _handle.ScenarioId;
|
||||
public string ScenarioSlug => _handle.ScenarioSlug;
|
||||
|
||||
/// <summary>Node id.</summary>
|
||||
public string NodeId => _handle.NodeId;
|
||||
|
||||
@@ -18,7 +18,7 @@ public sealed class WaitEffect
|
||||
public string RunId => _handle.RunId;
|
||||
|
||||
/// <summary>Scenario id.</summary>
|
||||
public string ScenarioId => _handle.ScenarioId;
|
||||
public string ScenarioSlug => _handle.ScenarioSlug;
|
||||
|
||||
/// <summary>Node id.</summary>
|
||||
public string NodeId => _handle.NodeId;
|
||||
|
||||
+62
-15
@@ -21,7 +21,7 @@ public sealed class EffectsService
|
||||
|
||||
private readonly RudderClient _client;
|
||||
private readonly object _gate = new();
|
||||
private readonly HashSet<(string RunId, string NodeId)> _seen = new();
|
||||
private readonly Dictionary<(string RunId, string NodeId), string> _seen = new();
|
||||
private readonly Dictionary<(string RunId, string NodeId), DateTimeOffset> _waitDeadlines = new();
|
||||
|
||||
private bool _refreshDue;
|
||||
@@ -99,8 +99,9 @@ public sealed class EffectsService
|
||||
continue;
|
||||
|
||||
var key = (effect.RunId, effect.NodeId);
|
||||
if (!_seen.Add(key))
|
||||
if (_seen.ContainsKey(key))
|
||||
continue;
|
||||
_seen[key] = effect.ScenarioSlug;
|
||||
|
||||
if (string.Equals(effect.Type, EffectTypes.Wait, StringComparison.Ordinal))
|
||||
{
|
||||
@@ -125,7 +126,7 @@ public sealed class EffectsService
|
||||
"/sdk/v1/scenarios/callback",
|
||||
new HandleScenarioCallbackRequest
|
||||
{
|
||||
ScenarioId = source.ScenarioId,
|
||||
ScenarioSlug = source.ScenarioSlug,
|
||||
NodeId = source.NodeId,
|
||||
Handle = handle,
|
||||
RunId = source.RunId
|
||||
@@ -135,13 +136,13 @@ public sealed class EffectsService
|
||||
ForgetWait(source.RunId, source.NodeId);
|
||||
var next = ReadEffect(response?.Effect);
|
||||
if (next == null)
|
||||
Emit(OnScenarioCompleted, new ScenarioCompletedEffect(source.RunId, source.ScenarioId));
|
||||
Emit(OnScenarioCompleted, new ScenarioCompletedEffect(source.RunId, source.ScenarioSlug));
|
||||
else
|
||||
Ingest(new[] { next });
|
||||
}
|
||||
catch (Exception ex) when (IsDefinitiveRejection(ex))
|
||||
{
|
||||
DropRun(source.RunId, source.ScenarioId, source.NodeId, ex);
|
||||
DropRun(source.RunId, source.ScenarioSlug, source.NodeId, ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +159,7 @@ public sealed class EffectsService
|
||||
"/sdk/v1/scenarios/counter",
|
||||
new UpdateScenarioCounterRequest
|
||||
{
|
||||
ScenarioId = source.ScenarioId,
|
||||
ScenarioSlug = source.ScenarioSlug,
|
||||
NodeId = source.NodeId,
|
||||
CounterKey = counterKey,
|
||||
Amount = amount,
|
||||
@@ -172,13 +173,13 @@ public sealed class EffectsService
|
||||
ForgetWait(source.RunId, source.NodeId);
|
||||
var next = ReadEffect(response.Effect);
|
||||
if (next == null)
|
||||
Emit(OnScenarioCompleted, new ScenarioCompletedEffect(source.RunId, source.ScenarioId));
|
||||
Emit(OnScenarioCompleted, new ScenarioCompletedEffect(source.RunId, source.ScenarioSlug));
|
||||
else
|
||||
Ingest(new[] { next });
|
||||
}
|
||||
catch (Exception ex) when (IsDefinitiveRejection(ex))
|
||||
{
|
||||
DropRun(source.RunId, source.ScenarioId, source.NodeId, ex);
|
||||
DropRun(source.RunId, source.ScenarioSlug, source.NodeId, ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -215,7 +216,12 @@ public sealed class EffectsService
|
||||
"/sdk/v1/scenarios/pending",
|
||||
CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
Ingest(response?.Effects);
|
||||
if (response != null)
|
||||
{
|
||||
Ingest(response.Effects);
|
||||
Reconcile(response.Effects);
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
_nextHeartbeat = _client.Clock.UtcNow + HeartbeatInterval;
|
||||
}
|
||||
@@ -268,19 +274,19 @@ public sealed class EffectsService
|
||||
OnScenarioFailed,
|
||||
new ScenarioFailedEffect(
|
||||
effect.RunId,
|
||||
effect.ScenarioId,
|
||||
effect.ScenarioSlug,
|
||||
effect.NodeId,
|
||||
new Exception($"Unsupported scenario node type '{effect.Type}'")));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void DropRun(string runId, string scenarioId, string nodeId, Exception exception)
|
||||
private void DropRun(string runId, string scenarioSlug, string nodeId, Exception exception)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
var toRemove = new List<(string RunId, string NodeId)>();
|
||||
foreach (var key in _seen)
|
||||
foreach (var key in _seen.Keys)
|
||||
{
|
||||
if (key.RunId == runId)
|
||||
toRemove.Add(key);
|
||||
@@ -293,7 +299,46 @@ public sealed class EffectsService
|
||||
}
|
||||
}
|
||||
|
||||
Emit(OnScenarioFailed, new ScenarioFailedEffect(runId, scenarioId, nodeId, exception));
|
||||
Emit(OnScenarioFailed, new ScenarioFailedEffect(runId, scenarioSlug, nodeId, exception));
|
||||
}
|
||||
|
||||
private void Reconcile(IEnumerable<PendingEffect>? effects)
|
||||
{
|
||||
var incoming = new HashSet<(string RunId, string NodeId)>();
|
||||
if (effects != null)
|
||||
{
|
||||
foreach (var effect in effects)
|
||||
{
|
||||
if (effect == null || string.IsNullOrEmpty(effect.RunId) || string.IsNullOrEmpty(effect.NodeId))
|
||||
continue;
|
||||
|
||||
incoming.Add((effect.RunId, effect.NodeId));
|
||||
}
|
||||
}
|
||||
|
||||
var finished = new Dictionary<string, string>();
|
||||
lock (_gate)
|
||||
{
|
||||
var stale = new List<(string RunId, string NodeId)>();
|
||||
foreach (var key in _seen.Keys)
|
||||
{
|
||||
if (!incoming.Contains(key))
|
||||
stale.Add(key);
|
||||
}
|
||||
|
||||
foreach (var key in stale)
|
||||
{
|
||||
finished[key.RunId] = _seen[key];
|
||||
_seen.Remove(key);
|
||||
_waitDeadlines.Remove(key);
|
||||
}
|
||||
|
||||
foreach (var key in _seen.Keys)
|
||||
finished.Remove(key.RunId);
|
||||
}
|
||||
|
||||
foreach (var entry in finished)
|
||||
Emit(OnScenarioCompleted, new ScenarioCompletedEffect(entry.Key, entry.Value));
|
||||
}
|
||||
|
||||
private void ForgetWait(string runId, string nodeId)
|
||||
@@ -347,7 +392,9 @@ public sealed class EffectsService
|
||||
return true;
|
||||
|
||||
return ex is RudderApiException api
|
||||
&& (api.Code == RudderErrorCodes.UnknownRun || api.Code == RudderErrorCodes.RunExpired);
|
||||
&& (api.Code == RudderErrorCodes.UnknownRun
|
||||
|| api.Code == RudderErrorCodes.RunExpired
|
||||
|| api.Code == RudderErrorCodes.RunNotActive);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,7 +424,7 @@ internal sealed class EffectHandle
|
||||
|
||||
public string RunId => _effect.RunId;
|
||||
|
||||
public string ScenarioId => _effect.ScenarioId;
|
||||
public string ScenarioSlug => _effect.ScenarioSlug;
|
||||
|
||||
public string NodeId => _effect.NodeId;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace RudderSdk.Core;
|
||||
public static class QuestMetrics
|
||||
{
|
||||
/// <summary>Metric for purchasing a store offer; auto-reported on purchase.</summary>
|
||||
public static string PurchaseOffer(string offerId) => $"purchase.offer:{offerId}";
|
||||
public static string PurchaseOffer(string offerSlug) => $"purchase.offer:{offerSlug}";
|
||||
|
||||
/// <summary>Metric for purchasing a catalog item; auto-reported on purchase.</summary>
|
||||
public static string PurchaseItem(string itemId) => $"purchase.item:{itemId}";
|
||||
|
||||
@@ -27,11 +27,11 @@ public sealed class QuestsService
|
||||
}
|
||||
|
||||
/// <summary>Claims a completed quest's rewards (idempotent server-side).</summary>
|
||||
public Task<ClaimQuestResponse> ClaimAsync(string questId, CancellationToken cancellationToken = default)
|
||||
public Task<ClaimQuestResponse> ClaimAsync(string questSlug, CancellationToken cancellationToken = default)
|
||||
=> _client.SendAsync<ClaimQuestRequest, ClaimQuestResponse>(
|
||||
"POST",
|
||||
"/sdk/v1/quests/claim",
|
||||
new ClaimQuestRequest { QuestId = questId },
|
||||
new ClaimQuestRequest { QuestSlug = questSlug },
|
||||
cancellationToken);
|
||||
|
||||
/// <summary>Reports progress for a metric and returns the ids of quests completed by this report.</summary>
|
||||
@@ -43,6 +43,6 @@ public sealed class QuestsService
|
||||
new ReportQuestProgressRequest { Metric = metric, Amount = amount },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return response?.CompletedQuestIds ?? new List<string>();
|
||||
return response?.CompletedQuestSlugs ?? new List<string>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,17 +35,17 @@ public sealed class StoresService
|
||||
/// </summary>
|
||||
public Task<PurchaseOfferResponse> PurchaseAsync(
|
||||
string storeSlug,
|
||||
string offerId,
|
||||
string offerSlug,
|
||||
string? idempotencyKey = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _client.SendAsync<PurchaseOfferRequest, PurchaseOfferResponse>(
|
||||
"POST",
|
||||
"/sdk/v1/stores/" + Url.Encode(storeSlug) + "/offers/" + Url.Encode(offerId) + "/purchase",
|
||||
"/sdk/v1/stores/" + Url.Encode(storeSlug) + "/offers/" + Url.Encode(offerSlug) + "/purchase",
|
||||
new PurchaseOfferRequest
|
||||
{
|
||||
StoreSlug = storeSlug,
|
||||
OfferId = offerId,
|
||||
OfferSlug = offerSlug,
|
||||
IdempotencyKey = idempotencyKey ?? Guid.NewGuid().ToString()
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
@@ -25,6 +25,9 @@ public class Offer
|
||||
[JsonProperty("price")]
|
||||
public OfferPrice? Price { get; set; }
|
||||
|
||||
[JsonProperty("slug")]
|
||||
public string? Slug { get; set; }
|
||||
|
||||
[JsonProperty("updatedAt")]
|
||||
public DateTimeOffset? UpdatedAt { get; set; }
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ public class PurchaseOfferRequest
|
||||
[JsonProperty("idempotencyKey")]
|
||||
public string? IdempotencyKey { get; set; }
|
||||
|
||||
[JsonProperty("offerId")]
|
||||
public string? OfferId { get; set; }
|
||||
[JsonProperty("offerSlug")]
|
||||
public string? OfferSlug { get; set; }
|
||||
|
||||
[JsonProperty("storeSlug")]
|
||||
public string? StoreSlug { get; set; }
|
||||
|
||||
@@ -82,7 +82,7 @@ realtime websocket client in Rudder.Core.
|
||||
Important cross-cutting facts:
|
||||
|
||||
- Battle pass state is tied to a scenario battle-pass node: every
|
||||
`BattlePassService` call carries `ScenarioId`/`NodeId` (mutations also
|
||||
`BattlePassService` call carries `ScenarioSlug`/`NodeId` (mutations also
|
||||
`RunId`). During a scenario run, `BattlePassEffect` supplies them — prefer
|
||||
the effect API inside a run.
|
||||
- Global quests (`client.Quests`) are distinct from scenario quest nodes
|
||||
|
||||
@@ -24,6 +24,19 @@ public void Logout(); // drops the stored session, fires SignedOut
|
||||
`RudderAuthState`: `SignedIn`, `SignedOut`. The event fires after a
|
||||
successful login and after logout or a failed token refresh.
|
||||
|
||||
## Environments
|
||||
|
||||
A project has two environments, `staging` and `prod`. The SDK key configured
|
||||
in `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 refresh pipeline then fails, clears the store and fires
|
||||
`AuthStateChanged(SignedOut)` — log the player in again.
|
||||
|
||||
## Endpoints
|
||||
|
||||
- `POST /sdk/v1/authorization/device` — body `{key, deviceId, region,
|
||||
|
||||
@@ -6,7 +6,7 @@ Source: `Services/BattlePassService.cs`,
|
||||
(generated DTOs).
|
||||
|
||||
Battle pass state is tied to a scenario battle-pass node, so every call
|
||||
carries `ScenarioId`/`NodeId` (mutations also `RunId`). Inside a scenario run,
|
||||
carries `ScenarioSlug`/`NodeId` (mutations also `RunId`). Inside a scenario run,
|
||||
`Effects.OnBattlePass` hands you a `BattlePassEffect` that supplies these
|
||||
ids — prefer it over calling the service directly.
|
||||
|
||||
@@ -18,7 +18,7 @@ public const string TrackPremium = "premium";
|
||||
|
||||
// POST /sdk/v1/battlepass/progress
|
||||
public Task<GetBattlePassProgressResponse> GetProgressAsync(
|
||||
string scenarioId, string nodeId, CancellationToken cancellationToken = default);
|
||||
string scenarioSlug, string nodeId, CancellationToken cancellationToken = default);
|
||||
|
||||
// POST /sdk/v1/battlepass/xp
|
||||
public Task<AddBattlePassXpResponse> AddXpAsync(
|
||||
@@ -40,7 +40,7 @@ public Task<PurchaseBattlePassPremiumResponse> PurchasePremiumAsync(
|
||||
public class GetBattlePassProgressRequest
|
||||
{
|
||||
public string? NodeId { get; set; } // "nodeId"
|
||||
public string? ScenarioId { get; set; } // "scenarioId"
|
||||
public string? ScenarioSlug { get; set; } // "scenarioSlug"
|
||||
}
|
||||
|
||||
public class AddBattlePassXpRequest
|
||||
@@ -48,7 +48,7 @@ public class AddBattlePassXpRequest
|
||||
public long? Amount { get; set; } // "amount"
|
||||
public string? NodeId { get; set; } // "nodeId"
|
||||
public string? RunId { get; set; } // "runId"
|
||||
public string? ScenarioId { get; set; } // "scenarioId"
|
||||
public string? ScenarioSlug { get; set; } // "scenarioSlug"
|
||||
public string? Source { get; set; } // "source"
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ public class ClaimBattlePassRewardRequest
|
||||
public int? Level { get; set; } // "level"
|
||||
public string? NodeId { get; set; } // "nodeId"
|
||||
public string? RunId { get; set; } // "runId"
|
||||
public string? ScenarioId { get; set; } // "scenarioId"
|
||||
public string? ScenarioSlug { get; set; } // "scenarioSlug"
|
||||
public string? Track { get; set; } // "track" — TrackFree / TrackPremium
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ public class PurchaseBattlePassPremiumRequest
|
||||
public string? IdempotencyKey { get; set; } // "idempotencyKey"
|
||||
public string? NodeId { get; set; } // "nodeId"
|
||||
public string? RunId { get; set; } // "runId"
|
||||
public string? ScenarioId { get; set; } // "scenarioId"
|
||||
public string? ScenarioSlug { get; set; } // "scenarioSlug"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -116,7 +116,7 @@ Raised via `Effects.OnBattlePass`. Bound to the node's scenario/node/run ids:
|
||||
|
||||
```csharp
|
||||
public string RunId { get; }
|
||||
public string ScenarioId { get; }
|
||||
public string ScenarioSlug { get; }
|
||||
public string NodeId { get; }
|
||||
public T Get<T>(string key, T defaultValue = default!);
|
||||
|
||||
|
||||
@@ -14,10 +14,10 @@ scenarios.md.
|
||||
public Task<IReadOnlyList<Quest>> ListAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
// POST /sdk/v1/quests/claim — claims a completed quest's rewards (idempotent server-side)
|
||||
public Task<ClaimQuestResponse> ClaimAsync(string questId, CancellationToken cancellationToken = default);
|
||||
public Task<ClaimQuestResponse> ClaimAsync(string questSlug, CancellationToken cancellationToken = default);
|
||||
|
||||
// POST /sdk/v1/quests/progress — reports progress for a metric; returns the
|
||||
// ids of quests completed by THIS report
|
||||
// slugs of quests completed by THIS report
|
||||
public Task<IReadOnlyList<string>> ReportProgressAsync(
|
||||
string metric, long amount, CancellationToken cancellationToken = default);
|
||||
```
|
||||
@@ -27,7 +27,7 @@ public Task<IReadOnlyList<string>> ReportProgressAsync(
|
||||
```csharp
|
||||
public static class QuestMetrics
|
||||
{
|
||||
public static string PurchaseOffer(string offerId); // "purchase.offer:{offerId}"
|
||||
public static string PurchaseOffer(string offerSlug); // "purchase.offer:{offerSlug}"
|
||||
public static string PurchaseItem(string itemId); // "purchase.item:{itemId}"
|
||||
}
|
||||
```
|
||||
@@ -40,10 +40,10 @@ metric is a custom string passed to `ReportProgressAsync`.
|
||||
```csharp
|
||||
public class Quest
|
||||
{
|
||||
public string? Id { get; set; } // "id"
|
||||
public string? Name { get; set; } // "name"
|
||||
public List<QuestObjectiveProgress>? Objectives { get; set; }// "objectives"
|
||||
public List<Reward>? Rewards { get; set; } // "rewards"
|
||||
public string? Slug { get; set; } // "slug" — stable across environments
|
||||
public string? Status { get; set; } // "status"
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,9 @@ effect. There is no local plan, no `IPlanStateStore`, and no restore.
|
||||
- This SDK does **not** fire a login scenario event (`loginEvent` /
|
||||
`player_login` is a web-SDK option only). Trigger login-gated scenarios
|
||||
yourself with `TriggerAsync`.
|
||||
- An effect with an already-seen `(runId, nodeId)` is not re-emitted.
|
||||
- An effect with an already-seen `(runId, nodeId)` is not re-emitted. The
|
||||
seen set is pruned against every pending fetch: keys the server no longer
|
||||
lists are dropped, so the server stays the source of truth.
|
||||
- There is no local plan persistence. Pending work lives on the server.
|
||||
|
||||
## `ScenarioService` (`client.Scenario`)
|
||||
@@ -76,7 +78,7 @@ client.Effects.OnNotification += n =>
|
||||
await client.Scenario.TriggerAsync("level_complete");
|
||||
```
|
||||
|
||||
Every effect exposes `RunId`, `ScenarioId`, `NodeId`, `Data` (`JObject`),
|
||||
Every effect exposes `RunId`, `ScenarioSlug`, `NodeId`, `Data` (`JObject`),
|
||||
and `Get<T>(key, defaultValue)` for reading node data. Completion methods
|
||||
have an `Async` variant (awaitable) and a fire-and-forget variant.
|
||||
|
||||
@@ -154,7 +156,7 @@ Scenario quest node — distinct from global `client.Quests`.
|
||||
public string Name { get; } // node data "name"
|
||||
public IReadOnlyList<JObject> Objectives { get; } // node data "objectives"
|
||||
|
||||
// POST /sdk/v1/scenarios/counter {scenarioId, nodeId, runId, counterKey, amount}
|
||||
// POST /sdk/v1/scenarios/counter {scenarioSlug, nodeId, runId, counterKey, amount}
|
||||
public Task ReportProgressAsync(string objectiveId, long amount = 1, CancellationToken ct = default);
|
||||
public void ReportProgress(string objectiveId, long amount = 1);
|
||||
```
|
||||
@@ -164,7 +166,7 @@ completed counter response may carry the next `PendingEffect`.
|
||||
|
||||
### `BattlePassEffect`
|
||||
|
||||
Bound to this node's `ScenarioId` / `NodeId` / `RunId` — prefer these over
|
||||
Bound to this node's `ScenarioSlug` / `NodeId` / `RunId` — prefer these over
|
||||
calling `client.BattlePass` by hand. See also reference/battlepass.md.
|
||||
|
||||
```csharp
|
||||
@@ -197,29 +199,31 @@ configured level.
|
||||
public sealed class ScenarioCompletedEffect
|
||||
{
|
||||
public string RunId { get; }
|
||||
public string ScenarioId { get; }
|
||||
public string ScenarioSlug { get; }
|
||||
}
|
||||
|
||||
public sealed class ScenarioFailedEffect
|
||||
{
|
||||
public string RunId { get; }
|
||||
public string ScenarioId { get; }
|
||||
public string ScenarioSlug { get; }
|
||||
public string NodeId { get; }
|
||||
public Exception Exception { get; }
|
||||
}
|
||||
```
|
||||
|
||||
`OnScenarioCompleted` fires when a callback / completed-counter response has
|
||||
no next effect (the run finished all its nodes).
|
||||
no next effect (the run finished all its nodes). It also fires when a run
|
||||
disappears from the pending list with no keys left for it — the server
|
||||
advanced a wait, the run expired, or another device completed it.
|
||||
|
||||
## Reliability
|
||||
|
||||
- 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`. A non-null `effect` in the response is ingested as
|
||||
the next node; a null/missing effect completes the run.
|
||||
- `unknown_run`, `run_expired` (`RudderErrorCodes`), and HTTP 404
|
||||
- `unknown_run`, `run_expired`, `run_not_active` (`RudderErrorCodes`), and HTTP 404
|
||||
(`RudderNotFoundException`) on a callback or counter call **drop that
|
||||
run** (forget its seen keys and wait deadlines) and fire
|
||||
`OnScenarioFailed`.
|
||||
@@ -239,7 +243,7 @@ public class PendingEffect
|
||||
public JToken Data { get; set; }
|
||||
public string NodeId { get; set; }
|
||||
public string RunId { get; set; }
|
||||
public string ScenarioId { get; set; }
|
||||
public string ScenarioSlug { get; set; }
|
||||
public string Type { get; set; }
|
||||
public DateTimeOffset? WaitDeadline { get; set; }
|
||||
}
|
||||
@@ -253,7 +257,7 @@ public class HandleScenarioCallbackRequest
|
||||
public string? Handle { get; set; }
|
||||
public string? NodeId { get; set; }
|
||||
public string? RunId { get; set; }
|
||||
public string? ScenarioId { get; set; }
|
||||
public string? ScenarioSlug { get; set; }
|
||||
}
|
||||
public class HandleScenarioCallbackResponse { public JToken? Effect { get; set; } }
|
||||
|
||||
@@ -263,7 +267,7 @@ public class UpdateScenarioCounterRequest
|
||||
public string? CounterKey { get; set; }
|
||||
public string? NodeId { get; set; }
|
||||
public string? RunId { get; set; }
|
||||
public string? ScenarioId { get; set; }
|
||||
public string? ScenarioSlug { get; set; }
|
||||
}
|
||||
public class UpdateScenarioCounterResponse
|
||||
{
|
||||
|
||||
@@ -14,11 +14,11 @@ public Task<IReadOnlyList<Store>> ListAsync(CancellationToken cancellationToken
|
||||
// GET /sdk/v1/stores/{slug}
|
||||
public Task<Store> GetAsync(string slug, CancellationToken cancellationToken = default);
|
||||
|
||||
// POST /sdk/v1/stores/{storeSlug}/offers/{offerId}/purchase
|
||||
// POST /sdk/v1/stores/{storeSlug}/offers/{offerSlug}/purchase
|
||||
// When idempotencyKey is null a random GUID is generated; pass a stable key to
|
||||
// make retries safe.
|
||||
public Task<PurchaseOfferResponse> PurchaseAsync(
|
||||
string storeSlug, string offerId, string? idempotencyKey = null,
|
||||
string storeSlug, string offerSlug, string? idempotencyKey = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
```
|
||||
|
||||
@@ -44,6 +44,7 @@ public class Store
|
||||
public class Offer
|
||||
{
|
||||
public string? Id { get; set; } // "id"
|
||||
public string? Slug { get; set; } // "slug" — stable across environments
|
||||
public string? Name { get; set; } // "name"
|
||||
public OfferPrice? Price { get; set; } // "price"
|
||||
public List<OfferContent>? Contents { get; set; }// "contents"
|
||||
@@ -65,5 +66,5 @@ public class PurchaseOfferResponse
|
||||
|
||||
`PurchaseOfferResponse.Error` carries a business error string when
|
||||
`Success` is false — check the flag instead of relying on exceptions alone.
|
||||
Purchases also auto-report quest metrics (`purchase.offer:{offerId}`) — see
|
||||
Purchases also auto-report quest metrics (`purchase.offer:{offerSlug}`) — see
|
||||
quests.md.
|
||||
|
||||
@@ -103,7 +103,7 @@ public sealed class ScenarioServiceTests
|
||||
|
||||
Assert.NotNull(completed);
|
||||
Assert.Equal("run-1", completed!.RunId);
|
||||
Assert.Equal("scenario-1", completed.ScenarioId);
|
||||
Assert.Equal("scenario-1", completed.ScenarioSlug);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -522,7 +522,7 @@ public sealed class ScenarioServiceTests
|
||||
return new PendingEffect
|
||||
{
|
||||
RunId = runId,
|
||||
ScenarioId = "scenario-1",
|
||||
ScenarioSlug = "scenario-1",
|
||||
NodeId = nodeId,
|
||||
Type = type,
|
||||
Data = data == null ? new JObject() : JObject.FromObject(data),
|
||||
|
||||
Reference in New Issue
Block a user