diff --git a/docs.json b/docs.json index 920c86ab..f9348742 100644 --- a/docs.json +++ b/docs.json @@ -868,6 +868,7 @@ "redis/tutorials/python_url_shortener", "redis/tutorials/api_with_cdk", "redis/tutorials/agent_memory", + "redis/tutorials/analytics_with_redis", "redis/tutorials/auto_complete_with_serverless_redis", "redis/tutorials/aws_app_runner_with_redis", "redis/tutorials/cloud_run_sessions", diff --git a/llms-full.txt b/llms-full.txt index 9884e52c..30bb92cd 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -18849,6 +18849,39 @@ We believe that Upstash is the best storage for your Lambda Functions because: * Designed for low latency data access * The lovely simple RedisĀ® API +## Detailed Tutorials + + + + Count and query events with counters, bitmaps, and Redis Search + + + Short-term and long-term memory for AI agents on Upstash Redis + + + Speed up your Laravel application with Upstash Redis caching + + + Build realtime apps on Cloudflare Workers backed by Upstash Redis + + + - [AWS Lambda](https://upstash.com/docs/redis/quickstarts/aws-lambda.md) - [Azure Functions](https://upstash.com/docs/redis/quickstarts/azure-functions.md) - [Cloudflare Workers](https://upstash.com/docs/redis/quickstarts/cloudflareworkers.md) @@ -24581,6 +24614,39 @@ SEARCH.COUNT products '{"price": {"$lt": 150}}' +## Next Steps + + + + Define the fields you want to index and how they are matched + + + Learn the JSON-based query language with filters and operators + + + Group and summarize your indexed data with aggregation pipelines + + + Complete, real-world examples you can adapt to your own use cases + + + # Indices Source: https://upstash.com/docs/redis/search/index-management @@ -25155,6 +25221,39 @@ Upstash Redis Search is our first extension beyond the official Redis spec. Usin regex support, and more. * **Production-Ready Performance**: Built on Tantivy, a fast full-text search engine library written in Rust +## Next Steps + + + + Create your first index and run a query in a few lines of code + + + Define the fields you want to index and how they are matched + + + Learn the JSON-based query language with filters and operators + + + Complete, real-world examples you can adapt to your own use cases + + + # $boost Source: https://upstash.com/docs/redis/search/query-operators/boolean-operators/boost @@ -28755,6 +28854,320 @@ wait isn't needed. [Smooth Text Streaming in AI SDK v5](https://upstash.com/blog/smooth-streaming). * Learn more about what Redis Search can do in the [Search docs](/docs/redis/search/introduction). +# Building Analytics with Redis +Source: https://upstash.com/docs/redis/tutorials/analytics_with_redis + +Most teams reach for a dedicated analytics product the moment they want to count +something. But if you already run Redis (for caching, sessions, or rate limiting), +you are sitting on one of the best analytics engines available. + +Why Redis? + +* **It's already there.** No new vendor, no new pipeline, no nightly ETL job. You + write events from the same code that serves your requests. +* **It's fast.** Counters, sets, and bitmaps are O(1) or close to it. You can + increment a metric on every request without thinking about it, and read the + result back in single-digit milliseconds. +* **It's serverless-friendly.** With [Upstash Redis](https://upstash.com/) you talk + to it over HTTP, so it works from edge functions, Lambdas, and the browser-facing + routes of a Next.js app, exactly where analytics events originate. + +There are two broad philosophies for doing analytics on Redis. This tutorial walks +through both, when to use each, and the tooling we've built to make the second one +painless. + +## Two philosophies + +There are two ways to think about this: + +1. **Plan ahead.** Decide what you want to measure up front and record it in a + compact structure (like a bitmap) built to answer that exact question. +2. **Record everything.** Capture each event with its metadata, index it with Redis + Search, and figure out the questions later. + +| | Plan ahead | Record everything | +| --- | --- | --- | +| **You decide...** | the questions up front | the questions later | +| **Storage** | counters, bitmaps, sets | event documents (JSON) | +| **Cost** | tiny, fixed | grows with event volume | +| **Querying** | read the counter | filter / aggregate with Redis Search | +| **Good for** | DAU, funnels, feature usage | ad-hoc product analytics | + + +You don't have to pick one. Many apps use bitmaps for the handful of metrics they +watch daily, and event recording for everything they might want to explore later. + + +## Philosophy 1: Plan ahead + +If you know in advance what you want to measure, you can record it in a structure +that answers that exact question for almost no storage. The classic example is the +**bitmap**: one bit per user, per day. + +### Daily active users with a bitmap + +A bitmap is a string where you can flip individual bits by offset. Use the user ID +as the offset and you get a per-day "did this user show up?" record that costs one +bit per user, roughly **1.2 MB for 10 million users**. + +```ts +import { Redis } from "@upstash/redis"; + +const redis = Redis.fromEnv(); + +// Mark user 1234 as active today +function markActive(userId: number, day = today()) { + return redis.setbit(`active:${day}`, userId, 1); +} + +// How many unique users were active today? +function dailyActiveUsers(day = today()) { + // bitcount accepts a key alone at runtime, but the TS SDK wants an + // explicit byte range; 0..-1 covers the whole bitmap. + return redis.bitcount(`active:${day}`, 0, -1); +} + +function today() { + return new Date().toISOString().slice(0, 10); // "2024-06-15" +} +``` + +`setbit` is O(1) and `bitcount` counts the set bits in one pass. Done. + +### Combining days: weekly actives and retention + +Because each day is its own bitmap, you can answer questions about *ranges* of days +with bitwise operations, without storing anything extra. + +```ts +// Weekly active users: OR the last 7 daily bitmaps together +async function weeklyActiveUsers(days: string[]) { + if (days.length === 0) return 0; + const [first, ...rest] = days.map((d) => `active:${d}`); + await redis.bitop("or", "active:week", first, ...rest); + return redis.bitcount("active:week", 0, -1); +} + +// Retention: users active on BOTH day A and day B +async function retained(dayA: string, dayB: string) { + await redis.bitop("and", "tmp:retained", `active:${dayA}`, `active:${dayB}`); + return redis.bitcount("tmp:retained", 0, -1); +} +``` + +`OR` gives you "active on any of these days", `AND` gives you "active on all of +them", the building blocks of retention and funnel analysis. The same idea powers +feature-adoption flags: keep a bitmap per feature and `AND` it against your active +users to see adoption. + +### When this breaks down + +The catch is right there in the name: you have to **plan ahead**. A bitmap answers +the one question you designed it for. The moment someone asks "okay, but how many of +those users were on mobile, in Germany, on the new checkout flow?" you're stuck: +that dimension was never recorded. You'd need to have created a separate bitmap for +every combination in advance, which doesn't scale. + +That's where the second philosophy comes in. + +## Philosophy 2: Record events, query later + +Instead of deciding the questions up front, record each event as a document with +whatever metadata you have on hand, index it with +[Redis Search](/docs/redis/search/introduction), and ask your questions *afterwards*. + +The flow is: + +1. Write each event as a JSON document under a known key prefix. +2. Define a search index over that prefix once. +3. Query and aggregate however you like (filters, ranges, group-bys) without + having planned for any specific question. + +### Define the index + +You define the index a single time. Redis Search then automatically picks up any +key matching the prefix; there is no separate "insert into index" step. + +```ts +import { Redis, s } from "@upstash/redis"; + +const redis = Redis.fromEnv(); + +const events = await redis.search.createIndex({ + name: "events-idx", + prefix: "event:", + dataType: "json", + existsOk: true, // don't throw if the index already exists + schema: s.object({ + name: s.keyword(), // "pageview", "signup", "purchase" + path: s.keyword(), // "/pricing" + country: s.keyword(), // "DE" + device: s.keyword(), // "mobile" + amount: s.number("F64"), // for purchase events + ts: s.date(), + }), +}); +``` + +### Record events + +Events are just JSON written with a regular Redis command. The index picks them up +on its own. + +```ts +async function track(event: { + name: string; + path?: string; + country?: string; + device?: string; + amount?: number; +}) { + const id = crypto.randomUUID(); + await redis.json.set(`event:${id}`, "$", { + ...event, + ts: new Date().toISOString(), + }); +} + +await track({ name: "pageview", path: "/pricing", country: "DE", device: "mobile" }); +await track({ name: "purchase", amount: 49.0, country: "DE", device: "mobile" }); +``` + + +Indexing is asynchronous. In a long-running app you don't need to think about it, +but in a tight loop, like a script or a test that writes events and immediately +queries them, call `await events.waitIndexing()` first so the documents you just +wrote are searchable: + +```ts +await events.waitIndexing(); +``` + + +### Now ask anything + +Here's the payoff. None of these queries needed to be anticipated when you recorded +the events. + +```ts +// How many mobile pageviews from Germany? +const { count } = await events.count({ + filter: { + $and: [ + { name: { $eq: "pageview" } }, + { device: { $eq: "mobile" } }, + { country: { $eq: "DE" } }, + ], + }, +}); + +// Total revenue and average order value, broken down by country +const revenue = await events.aggregate({ + filter: { name: { $eq: "purchase" } }, + aggregations: { + by_country: { + $terms: { field: "country", size: 20 }, + $aggs: { + total: { $avg: { field: "amount" } }, + orders: { $count: { field: "amount" } }, + }, + }, + }, +}); + +// Top pages this week +const topPages = await events.aggregate({ + filter: { ts: { $gte: "2024-06-10T00:00:00Z" } }, // s.date() expects an RFC 3339 string + aggregations: { + pages: { $terms: { field: "path", size: 10 } }, + }, +}); +``` + +Filters, numeric ranges, date ranges, group-bys (`$terms`), histograms, facets, +percentiles: all available, all decided at query time. See the +[querying](/docs/redis/search/querying) and [aggregating](/docs/redis/search/aggregations) +docs for the full set. + +## The complexity, and a PoC that hides it + +The event-recording approach is more flexible, but it does come with moving parts +that the simple examples above gloss over: + +* **Schema management**: keeping the index schema in sync as your events evolve. +* **Capturing events from the frontend**: you need an endpoint, batching, and a + client to send events without slowing down the page. +* **Sessions and context**: tying events together and attaching shared metadata. +* **Exploring the data**: a query API is not a dashboard. + +To explore how far this can be smoothed over, we built a proof-of-concept SDK, +[**`@upstash/redis-analytics`**](https://github.com/upstash/redis-analytics), that packages these pieces: + +* A **ready-to-use React hook** that auto-creates a session and captures pageviews. +* A single **backend endpoint** you drop into your app (e.g. a Next.js route). +* **Middleware** hooks for resolving feature flags and attaching server-side context. +* A **schema registry** that infers field types from your events and maintains the + Redis Search index for you. +* An **admin dashboard** for exploring captured analytics. + +A minimal end-to-end wiring looks like this: + +```ts +// lib/analytics.ts (backend client) +import { AnalyticsBackendClient } from "@upstash/redis-analytics"; + +export const analytics = new AnalyticsBackendClient({ + redis: { + url: process.env.UPSTASH_REDIS_REST_URL!, + token: process.env.UPSTASH_REDIS_REST_TOKEN!, + }, +}); +``` + +```ts +// app/api/analytics/route.ts (the single endpoint) +import { analytics } from "@/lib/analytics"; + +const handler = analytics.getHandler(); +export const POST = handler; +export const GET = handler; +``` + +```tsx +// frontend (the hook) +"use client"; +import { createAnalyticsHook } from "@upstash/redis-analytics/react"; + +const useAnalytics = createAnalyticsHook({ endpoint: "/api/analytics" }); + +export function BuyButton() { + const { captureEvent, sessionId } = useAnalytics(); + return ( + + ); +} +``` + +## Which should you use? + +* Reach for **bitmaps and counters** when you have a short, stable list of metrics + you watch every day. They're nearly free and answer instantly. +* Reach for **event recording with Redis Search** when you want to explore your data + and can't predict every question in advance. + +Both run on the Redis you already have. Start with whichever matches the questions +you have today; you can always add the other later. + # Deploy a Serverless API with AWS CDK and AWS Lambda Source: https://upstash.com/docs/redis/tutorials/api_with_cdk diff --git a/llms.txt b/llms.txt index 15a1b40a..af527dbb 100644 --- a/llms.txt +++ b/llms.txt @@ -788,6 +788,7 @@ - [Connecting with Read-Only Access](https://upstash.com/docs/redis/troubleshooting/readonly_connection.md) - [ERR XReadGroup is cancelled](https://upstash.com/docs/redis/troubleshooting/stream_pel_limit.md) - [Agent Memory with Redis Search](https://upstash.com/docs/redis/tutorials/agent_memory.md): Build short-term and long-term memory for AI agents on Upstash Redis. Store working memory with TTLs and recall long-term memories with Redis Search full-text queries. +- [Building Analytics with Redis](https://upstash.com/docs/redis/tutorials/analytics_with_redis.md): Two approaches to building analytics on Upstash Redis: pre-planned counters with bitmaps, and flexible event recording queried with Redis Search. - [Deploy a Serverless API with AWS CDK and AWS Lambda](https://upstash.com/docs/redis/tutorials/api_with_cdk.md) - [Autocomplete API with Serverless Redis](https://upstash.com/docs/redis/tutorials/auto_complete_with_serverless_redis.md) - [Build Stateful Applications with AWS App Runner and Serverless Redis](https://upstash.com/docs/redis/tutorials/aws_app_runner_with_redis.md): This tutorial shows how to create a serverless and stateful application using AWS App Runner and Redis diff --git a/redis/overall/usecases.mdx b/redis/overall/usecases.mdx index 89564f08..2d36181a 100755 --- a/redis/overall/usecases.mdx +++ b/redis/overall/usecases.mdx @@ -64,3 +64,36 @@ We believe that Upstash is the best storage for your Lambda Functions because: - Serverless just like Lambda functions itself - Designed for low latency data access - The lovely simple RedisĀ® API + +## Detailed Tutorials + + + + Count and query events with counters, bitmaps, and Redis Search + + + Short-term and long-term memory for AI agents on Upstash Redis + + + Speed up your Laravel application with Upstash Redis caching + + + Build realtime apps on Cloudflare Workers backed by Upstash Redis + + diff --git a/redis/search/getting-started.mdx b/redis/search/getting-started.mdx index 441b439e..07c00430 100644 --- a/redis/search/getting-started.mdx +++ b/redis/search/getting-started.mdx @@ -165,3 +165,36 @@ SEARCH.COUNT products '{"price": {"$lt": 150}}' + +## Next Steps + + + + Define the fields you want to index and how they are matched + + + Learn the JSON-based query language with filters and operators + + + Group and summarize your indexed data with aggregation pipelines + + + Complete, real-world examples you can adapt to your own use cases + + diff --git a/redis/search/introduction.mdx b/redis/search/introduction.mdx index b1b7d8a5..1a4619da 100644 --- a/redis/search/introduction.mdx +++ b/redis/search/introduction.mdx @@ -11,3 +11,36 @@ Upstash Redis Search is our first extension beyond the official Redis spec. Usin - **Intuitive Query Language**: A type-safe JSON-based query syntax with boolean operators, fuzzy matching, phrase queries, regex support, and more. - **Production-Ready Performance**: Built on Tantivy, a fast full-text search engine library written in Rust + +## Next Steps + + + + Create your first index and run a query in a few lines of code + + + Define the fields you want to index and how they are matched + + + Learn the JSON-based query language with filters and operators + + + Complete, real-world examples you can adapt to your own use cases + + diff --git a/redis/search/recipes/overview.mdx b/redis/search/recipes/overview.mdx index 4f7dca2d..dc3ab80a 100644 --- a/redis/search/recipes/overview.mdx +++ b/redis/search/recipes/overview.mdx @@ -22,3 +22,10 @@ These recipes demonstrate different patterns: - **Blog Search**: Optimized for content discovery with relevance ranking and visual feedback - **User Directory**: Optimized for quick lookups with autocomplete and exact matching +### Related Tutorials + +For more end-to-end examples that use Redis Search, see: + +- [Building Analytics with Redis](/redis/tutorials/analytics_with_redis): Record flexible events and query them with Redis Search. +- [Agent Memory with Redis Search](/redis/tutorials/agent_memory): Recall long-term memories for AI agents with full-text queries. + diff --git a/redis/tutorials/analytics_with_redis.mdx b/redis/tutorials/analytics_with_redis.mdx new file mode 100644 index 00000000..35ca77f8 --- /dev/null +++ b/redis/tutorials/analytics_with_redis.mdx @@ -0,0 +1,315 @@ +--- +title: Building Analytics with Redis +description: "Two approaches to building analytics on Upstash Redis: pre-planned counters with bitmaps, and flexible event recording queried with Redis Search." +--- + +Most teams reach for a dedicated analytics product the moment they want to count +something. But if you already run Redis (for caching, sessions, or rate limiting), +you are sitting on one of the best analytics engines available. + +Why Redis? + +- **It's already there.** No new vendor, no new pipeline, no nightly ETL job. You + write events from the same code that serves your requests. +- **It's fast.** Counters, sets, and bitmaps are O(1) or close to it. You can + increment a metric on every request without thinking about it, and read the + result back in single-digit milliseconds. +- **It's serverless-friendly.** With [Upstash Redis](https://upstash.com/) you talk + to it over HTTP, so it works from edge functions, Lambdas, and the browser-facing + routes of a Next.js app, exactly where analytics events originate. + +There are two broad philosophies for doing analytics on Redis. This tutorial walks +through both, when to use each, and the tooling we've built to make the second one +painless. + +## Two philosophies + +There are two ways to think about this: + +1. **Plan ahead.** Decide what you want to measure up front and record it in a + compact structure (like a bitmap) built to answer that exact question. +2. **Record everything.** Capture each event with its metadata, index it with Redis + Search, and figure out the questions later. + +| | Plan ahead | Record everything | +| --- | --- | --- | +| **You decide...** | the questions up front | the questions later | +| **Storage** | counters, bitmaps, sets | event documents (JSON) | +| **Cost** | tiny, fixed | grows with event volume | +| **Querying** | read the counter | filter / aggregate with Redis Search | +| **Good for** | DAU, funnels, feature usage | ad-hoc product analytics | + + +You don't have to pick one. Many apps use bitmaps for the handful of metrics they +watch daily, and event recording for everything they might want to explore later. + + +## Philosophy 1: Plan ahead + +If you know in advance what you want to measure, you can record it in a structure +that answers that exact question for almost no storage. The classic example is the +**bitmap**: one bit per user, per day. + +### Daily active users with a bitmap + +A bitmap is a string where you can flip individual bits by offset. Use the user ID +as the offset and you get a per-day "did this user show up?" record that costs one +bit per user, roughly **1.2 MB for 10 million users**. + +```ts +import { Redis } from "@upstash/redis"; + +const redis = Redis.fromEnv(); + +// Mark user 1234 as active today +function markActive(userId: number, day = today()) { + return redis.setbit(`active:${day}`, userId, 1); +} + +// How many unique users were active today? +function dailyActiveUsers(day = today()) { + // bitcount accepts a key alone at runtime, but the TS SDK wants an + // explicit byte range; 0..-1 covers the whole bitmap. + return redis.bitcount(`active:${day}`, 0, -1); +} + +function today() { + return new Date().toISOString().slice(0, 10); // "2024-06-15" +} +``` + +`setbit` is O(1) and `bitcount` counts the set bits in one pass. Done. + +### Combining days: weekly actives and retention + +Because each day is its own bitmap, you can answer questions about *ranges* of days +with bitwise operations, without storing anything extra. + +```ts +// Weekly active users: OR the last 7 daily bitmaps together +async function weeklyActiveUsers(days: string[]) { + if (days.length === 0) return 0; + const [first, ...rest] = days.map((d) => `active:${d}`); + await redis.bitop("or", "active:week", first, ...rest); + return redis.bitcount("active:week", 0, -1); +} + +// Retention: users active on BOTH day A and day B +async function retained(dayA: string, dayB: string) { + await redis.bitop("and", "tmp:retained", `active:${dayA}`, `active:${dayB}`); + return redis.bitcount("tmp:retained", 0, -1); +} +``` + +`OR` gives you "active on any of these days", `AND` gives you "active on all of +them", the building blocks of retention and funnel analysis. The same idea powers +feature-adoption flags: keep a bitmap per feature and `AND` it against your active +users to see adoption. + +### When this breaks down + +The catch is right there in the name: you have to **plan ahead**. A bitmap answers +the one question you designed it for. The moment someone asks "okay, but how many of +those users were on mobile, in Germany, on the new checkout flow?" you're stuck: +that dimension was never recorded. You'd need to have created a separate bitmap for +every combination in advance, which doesn't scale. + +That's where the second philosophy comes in. + +## Philosophy 2: Record events, query later + +Instead of deciding the questions up front, record each event as a document with +whatever metadata you have on hand, index it with +[Redis Search](/redis/search/introduction), and ask your questions *afterwards*. + +The flow is: + +1. Write each event as a JSON document under a known key prefix. +2. Define a search index over that prefix once. +3. Query and aggregate however you like (filters, ranges, group-bys) without + having planned for any specific question. + +### Define the index + +You define the index a single time. Redis Search then automatically picks up any +key matching the prefix; there is no separate "insert into index" step. + +```ts +import { Redis, s } from "@upstash/redis"; + +const redis = Redis.fromEnv(); + +const events = await redis.search.createIndex({ + name: "events-idx", + prefix: "event:", + dataType: "json", + existsOk: true, // don't throw if the index already exists + schema: s.object({ + name: s.keyword(), // "pageview", "signup", "purchase" + path: s.keyword(), // "/pricing" + country: s.keyword(), // "DE" + device: s.keyword(), // "mobile" + amount: s.number("F64"), // for purchase events + ts: s.date(), + }), +}); +``` + +### Record events + +Events are just JSON written with a regular Redis command. The index picks them up +on its own. + +```ts +async function track(event: { + name: string; + path?: string; + country?: string; + device?: string; + amount?: number; +}) { + const id = crypto.randomUUID(); + await redis.json.set(`event:${id}`, "$", { + ...event, + ts: new Date().toISOString(), + }); +} + +await track({ name: "pageview", path: "/pricing", country: "DE", device: "mobile" }); +await track({ name: "purchase", amount: 49.0, country: "DE", device: "mobile" }); +``` + + +Indexing is asynchronous. In a long-running app you don't need to think about it, +but in a tight loop, like a script or a test that writes events and immediately +queries them, call `await events.waitIndexing()` first so the documents you just +wrote are searchable: + +```ts +await events.waitIndexing(); +``` + + +### Now ask anything + +Here's the payoff. None of these queries needed to be anticipated when you recorded +the events. + +```ts +// How many mobile pageviews from Germany? +const { count } = await events.count({ + filter: { + $and: [ + { name: { $eq: "pageview" } }, + { device: { $eq: "mobile" } }, + { country: { $eq: "DE" } }, + ], + }, +}); + +// Total revenue and average order value, broken down by country +const revenue = await events.aggregate({ + filter: { name: { $eq: "purchase" } }, + aggregations: { + by_country: { + $terms: { field: "country", size: 20 }, + $aggs: { + total: { $avg: { field: "amount" } }, + orders: { $count: { field: "amount" } }, + }, + }, + }, +}); + +// Top pages this week +const topPages = await events.aggregate({ + filter: { ts: { $gte: "2024-06-10T00:00:00Z" } }, // s.date() expects an RFC 3339 string + aggregations: { + pages: { $terms: { field: "path", size: 10 } }, + }, +}); +``` + +Filters, numeric ranges, date ranges, group-bys (`$terms`), histograms, facets, +percentiles: all available, all decided at query time. See the +[querying](/redis/search/querying) and [aggregating](/redis/search/aggregations) +docs for the full set. + +## The complexity, and a PoC that hides it + +The event-recording approach is more flexible, but it does come with moving parts +that the simple examples above gloss over: + +- **Schema management**: keeping the index schema in sync as your events evolve. +- **Capturing events from the frontend**: you need an endpoint, batching, and a + client to send events without slowing down the page. +- **Sessions and context**: tying events together and attaching shared metadata. +- **Exploring the data**: a query API is not a dashboard. + +To explore how far this can be smoothed over, we built a proof-of-concept SDK, +[**`@upstash/redis-analytics`**](https://github.com/upstash/redis-analytics), that packages these pieces: + +- A **ready-to-use React hook** that auto-creates a session and captures pageviews. +- A single **backend endpoint** you drop into your app (e.g. a Next.js route). +- **Middleware** hooks for resolving feature flags and attaching server-side context. +- A **schema registry** that infers field types from your events and maintains the + Redis Search index for you. +- An **admin dashboard** for exploring captured analytics. + +A minimal end-to-end wiring looks like this: + +```ts +// lib/analytics.ts (backend client) +import { AnalyticsBackendClient } from "@upstash/redis-analytics"; + +export const analytics = new AnalyticsBackendClient({ + redis: { + url: process.env.UPSTASH_REDIS_REST_URL!, + token: process.env.UPSTASH_REDIS_REST_TOKEN!, + }, +}); +``` + +```ts +// app/api/analytics/route.ts (the single endpoint) +import { analytics } from "@/lib/analytics"; + +const handler = analytics.getHandler(); +export const POST = handler; +export const GET = handler; +``` + +```tsx +// frontend (the hook) +"use client"; +import { createAnalyticsHook } from "@upstash/redis-analytics/react"; + +const useAnalytics = createAnalyticsHook({ endpoint: "/api/analytics" }); + +export function BuyButton() { + const { captureEvent, sessionId } = useAnalytics(); + return ( + + ); +} +``` + +## Which should you use? + +- Reach for **bitmaps and counters** when you have a short, stable list of metrics + you watch every day. They're nearly free and answer instantly. +- Reach for **event recording with Redis Search** when you want to explore your data + and can't predict every question in advance. + +Both run on the Redis you already have. Start with whichever matches the questions +you have today; you can always add the other later.