diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..d8c0945 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +snippets/* diff --git a/api-reference/idempotency.mdx b/api-reference/idempotency.mdx new file mode 100644 index 0000000..7f9d8f0 --- /dev/null +++ b/api-reference/idempotency.mdx @@ -0,0 +1,65 @@ +--- +title: Idempotency +description: Prevent unintended side effects when retrying requests with idempotency controls built into the Trophy API and SDKs. +"og:description": Prevent unintended side effects when retrying requests with idempotency built into the Trophy API and SDKs using the `Idempotency-Key` request header. +icon: repeat +--- + +import IdempotentEventTracking from "/snippets/idempotent-event-tracking.mdx"; + +## What is Idempotency? + +When describing APIs, [idempotence](https://en.wikipedia.org/wiki/Idempotence) is a property of a particular operation whereby subsequent invocations after the first have no additional effect on the state of the system. + +Trophy's [event tracking API](/api-reference/endpoints/metrics/send-a-metric-change-event) can be used to enforce idempotency preventing client retries from having unintended side effects such as overcounting user interactions, or awarding points to users multiple times for the same action. + +This is particularly important where rewards are tied to user actions to prevent users from 'gaming the system'. + +## Sending Idempotent Requests + +To ensure idempotency is respected when sending events to Trophy, include an `Idempotency-Key` header when using the [event tracking API](/api-reference/endpoints/metrics/send-a-metric-change-event). Additionally, all [client SDKs](/api-reference/client-libraries) support idempotency with built-in type safety. + +You can choose what to use as your idempotency key, but it should reflect the level of 'uniqueness' that you want Trophy to respect. + + + For example, if you use Trophy to reward users for completing lessons, and + want to make sure each user can only redeem rewards once for each lesson, use + the unique ID of the lesson as your idempotency key. + + +Here's an example of what using an idempotency key looks like: + + + +## How Idempotency Works + +When Trophy detects an idempotency key has been sent with an event, it will first check if the user the event relates to has used it before. It will then proceed to take one of the following actions: + +- If the user has used the idempotency key before, Trophy will not process the event, returning a `202 Accepted` response. The response will reflect the current state of the system, but will not increase the users metric total, complete any achievements, award any points, extend the streak, etc. +- If instead Trophy detects the user hasn't used the idempotency key before, it will process the event as usual, returning a `201 Created` response. Finally Trophy will store the idempotency key for lookup during any subsequent requests. + + + All Trophy [metrics](/platform/metrics) manage idempotency in isolation. + Trophy will accept a user using the same idempotency key for events against + different metrics as seperate isolated requests. + + +Additionally, when using an idempotency key the response will contain two properties to help clients manage replayed requests effectively: + +```json +{ + ..., + "idempotentReplayed": true, // true if replayed, false otherwise + "idempotencyKey": "test" // the original idempotency key +} +``` + + + By default Trophy uses an infinte time window for detecting duplicate events. + If you feel you need different behavoir, please [get in + touch](mailto:support@trophy.so) and we'll happily set that up for you. + + +## Get Support + +Want to get in touch with the Trophy team? Reach out to us via [email](mailto:support@trophy.so). We're here to help! diff --git a/docs.json b/docs.json index 4e499f5..1c57e67 100644 --- a/docs.json +++ b/docs.json @@ -113,6 +113,7 @@ "api-reference/introduction", "api-reference/authentication", "api-reference/rate-limiting", + "api-reference/idempotency", "api-reference/client-libraries" ] }, diff --git a/platform/events.mdx b/platform/events.mdx index 3749dfb..f2c7ca7 100644 --- a/platform/events.mdx +++ b/platform/events.mdx @@ -194,6 +194,18 @@ Watch Charlie integrate metric tracking into a simple NextJS application using t > +### Idempotent Events + +Trophy supports enforcing uniqueness on events so that users cannot increase a metric by taking the same exact action over and over. + +For example, a language learning app could specify that users can only increase the `lessons completed` metric by 1 for each unique lesson completed, so if they complete the same lesson twice only the first counts. + +This helps keep your codebase free of logic that checks if users have completed actions before, and can instead trust Trophy to uphold the constraints you need. + +To use idempotent events, use the `Idempotency-Key` header in the [metric event API](/api-reference/endpoints/metrics/send-a-metric-change-event). + +[Learn more about idempotency](/api-reference/idempotency). + ## Get Support Want to get in touch with the Trophy team? Reach out to us via [email](mailto:support@trophy.so). We're here to help! diff --git a/snippets/idempotent-event-tracking.mdx b/snippets/idempotent-event-tracking.mdx new file mode 100644 index 0000000..ac71d8e --- /dev/null +++ b/snippets/idempotent-event-tracking.mdx @@ -0,0 +1,112 @@ + +```bash cURL {3} +curl -X POST https://app.trophy.so/api/metrics/lessons-completed/event \ + -H "X-API-KEY: " \ + -H "Idempotency-Key: " \ + -H "Content-Type: application/json" \ + -d '{ + "user": { + "id": "18", + "email": "user@example.com", + "tz": "Europe/London" + }, + "value": 1 +}' +``` + +```typescript Node {10-12} +trophy.metrics.event("lessons-completed", + { + user: { + id: "18", + email: "user@example.com", + tz: "Europe/London", + }, + value: 1, + }, + { + idempotencyKey: "lesson-123", + } +); +``` + +```python Python +client.metrics.event( + key="lessons-completed", + user=EventRequestUser( + id="18", + email="user@example.com", + tz="Europe/London", + ), + value=1, +) +``` + +```php PHP +$user = new EventRequestUser([ + 'id' => '18', + 'email' => 'user@example.com' +]); + +$request = new MetricsEventRequest([ + 'user' => $user, + 'value' => 1 +]); + +$trophy->metrics->event("lessons-completed", $request); +``` + +```java Java +MetricsEventRequest request = MetricsEventRequest.builder() + .user( + EventRequestUser.builder() + .id("18") + .email("user@example.com") + .build() + ) + .value(1) + .build(); + +EventResponse response = client.metrics().event("lessons-completed", request); +``` + +```go Go +response, err := client.Metrics.Event( + context.TODO(), + "lessons-completed", + &api.MetricsEventRequest{ + User: &api.EventRequestUser{ + Id: "18", + Email: "user@example.com", + }, + Value: 1, + }, +) +``` + +```csharp C# +var user = new EventRequestUser { + Id = "18", + Email = "user@example.com" +}; + +var request = new MetricsEventRequest { + User = user, + Value = 1 +}; + +await trophy.Metrics.EventAsync("lessons-completed", request); +``` + +```ruby Ruby +result = client.metrics.event( + :key => 'lessons-completed', + :user => { + :id => '18', + :email => 'user@example.com' + }, + :value => 1 +) +``` + +