# Welcome to Ripples.sh Lightweight analytics for indie products. One script tag. No complex setup. Free until $1K MRR. ## What is Ripples.sh? Ripples.sh gives you a pre-built analytics dashboard with traffic sources, signups, payments, and payback — all from a single script tag. No cookies banner needed for basic analytics, no account needed to start. Out of the box you get: - **Pageviews & sessions** — automatic tracking with SPA support - **Traffic sources** — referrers, UTM params, ad click IDs (Google, Meta, TikTok, etc.) - **Device & browser info** — OS, browser, screen size, language - **Web Vitals** — LCP, FCP, CLS, INP, TTFB with an experience score - **Bot filtering** — known crawlers are automatically excluded - **Visitor profiles** — anonymous and identified user tracking ## Quick start Add this script tag to your site, right before the closing `` tag: ```html ``` That's it. Pageviews, sessions, and Web Vitals will be tracked automatically. See [Installation](/docs/installation) for framework-specific guides. ## How it works The script (~3KB gzipped) runs in the browser and sends events to `https://api.ripples.sh/collect` using `sendBeacon`. Each event includes the page URL, referrer, UTM parameters, device info, and a randomly generated visitor ID stored in `localStorage`. Sessions are tracked per-tab with a 30-minute inactivity timeout. No server-side code is needed. ## SDK methods overview Beyond automatic pageview tracking, the JS SDK exposes these methods: | Method | Description | | --- | --- | | `[ripples.track()](/docs/custom-events)` | Track meaningful product usage — powers the Activation dashboard | | `[ripples.identify()](/docs/identify)` | Associate a visitor with an authenticated user | | `[ripples.pageview()](/docs/spa)` | Manually trigger a pageview (for edge cases) | | `[ripples.getVisitorId()](/docs/identify#get-visitor-id)` | Get the visitor's unique ID for server-to-server attribution | --- # Installation Add Ripples.sh to your site in under a minute. Works with any framework or static site. ## Script tag The simplest way to install. Add this to your HTML ``: ```html ``` ## Script attributes | Attribute | Description | | --- | --- | | `data-token` (required) | Your project token. Found in your dashboard under the install snippet. | | `data-endpoint` (optional) | Override the default collection endpoint. Useful for self-hosted setups or proxying through your own domain. | | `defer` (optional) | Recommended. Loads the script without blocking page render. | ## Next.js Add the script in your root layout: ```jsx import Script from 'next/script' export default function RootLayout({ children }) { return (
``` > SPA routing is handled automatically. The script listens to history.pushState and popstate events, so pageviews are tracked on every route change. No extra setup needed. ## Vue / Nuxt For Nuxt 3, add to your `nuxt.config.ts`: ```typescript export default defineNuxtConfig({ app: { head: { script: [ { src: 'https://cdn.ripples.sh/v.js', defer: true, 'data-token': 'YOUR_TOKEN', }, ], }, }, }) ``` ## Astro Add to your base layout component: ```html ``` ## Laravel / Blade Add to your main layout file: ```html ``` ## Verify installation After adding the script, open your site and check the browser's Network tab. You should see a POST request to `api.ripples.sh/collect`. Your dashboard will start showing data within seconds. ## Loading before the script is ready If you need to call SDK methods (like `ripples.track()`) before the script has loaded, use the queue pattern: ```html ``` Any calls made before the SDK loads will be replayed automatically once it initializes. --- # Track Product Usage Track meaningful user actions in your product with ripples.track(). Powers the Activation dashboard. ## ripples.track() Call `track()` when a user does something meaningful in your product: ```javascript ripples.track("created a budget", { area: "budgets", }) ``` Ripples auto-detects activation moments (first occurrence per user per action), computes adoption rates by product area, and correlates usage patterns with retention and payment. ### Parameters | Parameter | Type | Description | | --- | --- | --- | | `name` (required) | `string` | What the user did. Be specific: `"created a budget"`, not `"budgets"`. | | `options` (optional) | `object` | Options object with `area`, `activated`, and any custom properties. | ### Options | Key | Type | Description | | --- | --- | --- | | `area` (optional) | `string` | Product area this action belongs to (e.g. `"budgets"`, `"sharing"`). Groups actions in the dashboard. | | `activated` (optional) | `boolean` | Set to `true` on the specific occurrence when activation happens for this user. This does not mark the event type as an activation event — it marks this particular moment as when the user activated. For example, `"sent message"` is a regular event, but when a user sends their 10th message you may consider that their activation moment and send that occurrence with `activated: true`. | | `[custom]` (optional) | `string | number` | Any additional context. Values should be strings or numbers. | ## Examples ### Finance app ```javascript // User created a budget — group under "budgets" area ripples.track("created a budget", { area: "budgets" }) // User set a spending limit ripples.track("set budget limit", { area: "budgets", limit: "500" }) // User exported a report ripples.track("exported report", { area: "reports", format: "csv" }) ``` ### Personal finance tracker ```javascript // User added a transaction — everyday action ripples.track("added transaction", { area: "transactions" }) // User added their 10th transaction — we consider this their activation moment ripples.track("added transaction", { area: "transactions", activated: true, // only on THIS occurrence, not every "added transaction" }) // User exported a report ripples.track("exported report", { area: "reports", format: "csv" }) ``` ## Product areas Use the `area` option to group actions into zones of your product. Areas are auto-discovered from your `track()` calls — no setup required. They appear in the Activation dashboard as an adoption heatmap: ``` Product Areas Adoption → Paid ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Basic Tracking ████████████ 89% 6% Budgets ██████ 41% 18% Sharing ████ 27% 22% Reports ██ 14% 9% ``` ## Activation flag Use `activated: true` to mark the **specific moment** a user activates — not to label an event type as "the activation event." The same event name can be sent many times without the flag. You add `activated: true` only on the occurrence that represents the milestone. Your app decides when that is (e.g. 10th transaction added, first budget over $100). ```javascript // User added their 10th transaction — we consider this their activation ripples.track("added transaction", { area: "transactions", activated: true, // only on this occurrence }) // All other "added transaction" calls are normal — no activated flag ripples.track("added transaction", { area: "transactions" }) ``` ## Alternative: queue syntax You can also use the function-call style, which works even before the SDK has loaded: ```javascript ripples("track", "created a budget", { area: "budgets" }) ``` Both `ripples.track(...)` and `ripples("track", ...)` are equivalent. ## Best practices - **Be specific with action names** — `"created a budget"` is better than `"budgets"` or `"click"`. - **Use areas consistently** — pick area names that match how your product is organized. - **Track meaningful actions**, not every click. Focus on actions that indicate the user got value. - **Use `activated: true` on the specific occurrence** when you consider the user activated — not on every call of that event type. - Events are sent via `sendBeacon` so they won't block navigation or slow down your site. --- # Identify Users Associate anonymous visitors with authenticated users using ripples.identify(). > Note: Identifying users stores the user ID in localStorage. Once identified, all subsequent events (including pageviews) will include the user ID until the storage is cleared. ## ripples.identify() Call this method after a user logs in or signs up: ```javascript ripples.identify({ id: "user_123", // required email: "jane@acme.com", // optional name: "Jane Doe", // optional avatar_url: "https://...", // optional }) ``` ### Parameters | Parameter | Type | Description | | --- | --- | --- | | `props` (required) | `object` | User properties object. Must include `id`. | ### Properties object | Key | Type | Description | | --- | --- | --- | | `id` (required) | `string` | Your internal user ID. This links anonymous sessions to the user. | | `email` (optional) | `string` | User's email address. Shown in the visitor profile. | | `name` (optional) | `string` | Display name. Shown in the visitor profile instead of the anonymous name. | | `avatar_url` (optional) | `string` | URL to the user's avatar image. Also accepts `avatar` as an alias. Shown in the visitor profile. | | `[custom]` (optional) | `string | number` | Any additional properties you want to attach to the user (e.g. `plan`, `company`). | ## Examples ### After login ```javascript // After successful authentication async function onLoginSuccess(user) { ripples.identify({ id: user.id, email: user.email, name: user.name, plan: user.plan, }) } ``` ### React example ```javascript function useIdentify(user) { useEffect(() => { if (user) { ripples.identify({ id: user.id, email: user.email, name: user.displayName, }) } }, [user]) } ``` ## Alternative syntax You can also pass the user ID as a string with properties as a second argument: ```javascript // Object style (recommended) ripples.identify({ id: "user_123", name: "Jane" }) // Queue style ripples("identify", { id: "user_123", name: "Jane" }) // String ID style (legacy) ripples("identify", "user_123", { name: "Jane" }) ``` ## ripples.getVisitorId() Returns the current visitor's unique ID. Use this to link client-side activity with your backend — for example, to send server-to-server events. ```javascript const visitorId = ripples.getVisitorId() // "a1b2c3d4-e5f6-7890-abcd-ef1234567890" ``` A common pattern is to pass the visitor ID to your backend on signup or checkout, so you can attribute server-side events (like Stripe payments) back to the visitor: ```javascript // Send visitor ID with your API call await fetch('/api/signup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email, plan: plan, ripples_visitor_id: ripples.getVisitorId(), }), }) ``` ```php // Store the visitor ID alongside the user record // Later, use it to send server-side events via the API $user->ripples_visitor_id = $request->ripples_visitor_id; ``` > Visitor ID persistence: The visitor ID is stored in a cross-subdomain cookie (_rpl_vid) with a 2-year expiry, and also in localStorage as a fallback. This means app.yoursite.com and www.yoursite.com share the same visitor ID. ## How it works 1. When you call `ripples.identify()`, the user ID is saved to `localStorage` under the key `_rpl_uid`, and the avatar URL under `_rpl_uav`. 2. An `identify` event is sent to the collection endpoint with the user properties. 3. All future events (pageviews, tracks) will include this user ID automatically. 4. The visitor profile in your dashboard will show the user's name, email, and avatar instead of the anonymous identifier. > Cross-device tracking: Once a user is identified on one device, their activity will be linked across all devices where they log in. --- # SPA Support Single-page app routing is handled automatically. Here's how it works and when you might need manual control. ## Automatic tracking The SDK automatically tracks pageviews for single-page applications by listening to: - `history.pushState` — triggered by client-side navigation (React Router, Vue Router, Next.js, etc.) - `popstate` — triggered when the user clicks the browser's back/forward buttons No configuration is needed. Just install the script and every route change will be tracked. > Works with all major frameworks: React, Vue, Svelte, Angular, Next.js, Nuxt, SvelteKit, Astro (with client-side routing enabled). ## Manual pageview In rare cases where you need to trigger a pageview manually (e.g. after a hash change or a custom router), use: ```javascript ripples.pageview() ``` This sends a pageview event with the current `location.href`, `location.pathname`, and `document.title`. ## Session & entry page tracking Sessions are tracked per-tab using `sessionStorage`. A session expires after **30 minutes of inactivity**. Each session records: - **Entry URL** — the first page the user landed on in this session - **Entry path** — the path portion of the entry URL - **Entry referrer** — the domain that brought the user to your site These values persist across all pageviews within the same session, so you can always see where a session originally started — even after the user has navigated away from the landing page. ## Web Vitals The SDK automatically collects Core Web Vitals using the browser's `PerformanceObserver` API: | Metric | Description | | --- | --- | | `LCP` | Largest Contentful Paint — measures loading performance | | `FCP` | First Contentful Paint — time to first visible content | | `CLS` | Cumulative Layout Shift — measures visual stability | | `INP` | Interaction to Next Paint — measures responsiveness | | `TTFB` | Time to First Byte — measures server response time | These are combined into an **Experience Score** (0–100) shown in your dashboard. Vitals are reported once per page load, after at least 3 metrics have been collected (or after a 10-second timeout). --- # Stripe Integration Automatically track revenue and attribute it to the visitors who converted. ## Overview The Stripe integration connects your Stripe account to Ripples so you can: - See revenue attributed to each visitor and their acquisition source - Track charges, subscriptions, and invoices automatically - Backfill historical payment data when you first connect - Handle refunds (negative revenue events are created automatically) Revenue events appear in the visitor timeline and contribute to each profile's lifetime revenue total. ## Prerequisites Before connecting Stripe, make sure you have: 1. **Persist mode enabled** — Ripples uses a cookie (`_rpl_vid`) to track visitors across sessions. This is the default behavior. 2. **User identification** — Call `ripples.identify()` when users sign in so we can match Stripe charges to visitor profiles by email. ```javascript // Call this after login / signup ripples.identify({ id: "user_123", email: "jane@example.com", name: "Jane Doe", plan: "pro", }) ``` ## Create a Restricted API Key We recommend using a **restricted API key** with minimal permissions rather than your secret key. 1. Go to [Stripe Dashboard → API Keys](https://dashboard.stripe.com/apikeys) 2. Click **"Create restricted key"** 3. Give it a name like `Ripples Analytics` 4. Set the following permissions to **Read**: | Permission | Access | | --- | --- | | Charges | Read | | Customers | Read | | Checkout Sessions | Read | | Subscriptions | Read | | Invoices | Read | | Webhook Endpoints | Write | The **Webhook Endpoints: Write** permission allows Ripples to automatically create a webhook in your Stripe account to receive payment events in real-time. 5. Click **"Create key"** and copy the key (it starts with `rk_live_` or `rk_test_`) ## Connect in Settings 1. Open your project dashboard in Ripples 2. Click the **Integrations** link in the header 3. Find the **Stripe** card and click **Connect** 4. Paste your restricted API key and click **Connect** Once connected, Ripples will: - Create a webhook endpoint in your Stripe account automatically - Begin importing your historical charges (backfill) - Start receiving new payment events in real-time ## Revenue Attribution Ripples uses a **3-tier fallback** to attribute each Stripe payment to a visitor: ### Tier 1: Checkout Metadata (Best) Pass the visitor ID directly in your Stripe Checkout session metadata. This gives the most accurate attribution because it directly links the payment to the browsing session. ```javascript // Next.js (Server-side) import { cookies } from "next/headers" import Stripe from "stripe" const stripe = new Stripe(process.env.STRIPE_SECRET_KEY) export async function createCheckout() { const cookieStore = await cookies() const visitorId = cookieStore.get("_rpl_vid")?.value const session = await stripe.checkout.sessions.create({ metadata: { ripples_visitor_id: visitorId }, // ... other checkout options }) } ``` ```javascript // Vanilla JavaScript (Client-side) // Get visitor ID from the Ripples SDK const visitorId = ripples.getVisitorId() // Send to your server when creating a checkout session const res = await fetch("/api/checkout", { method: "POST", body: JSON.stringify({ visitorId }), }) ``` ```php // Laravel (Server-side) use Stripe\StripeClient; $visitorId = $request->cookie('_rpl_vid'); // or from frontend: $request->input('visitorId') $stripe = new StripeClient(config('services.stripe.secret')); $session = $stripe->checkout->sessions->create([ 'metadata' => ['ripples_visitor_id' => $visitorId], // ... other options ]); ``` ### Tier 2: Email Matching (Automatic) If Tier 1 metadata is not present, Ripples matches the Stripe customer email to a visitor profile. This works when you have called `ripples.identify()` with the user's email. No extra setup needed — this happens automatically. ### Tier 3: Customer ID (Returning Customers) For subsequent payments, Ripples remembers the Stripe customer ID from previous attributions. If a customer was attributed once (via Tier 1 or 2), all future payments from that customer are automatically attributed to the same visitor. ## Events Tracked The following Stripe events are processed: | Stripe Event | Ripples Event | Description | | --- | --- | --- | | `charge.succeeded` | Charge Succeeded | One-time or first-time payment | | `charge.refunded` | Charge Refunded | Refund (negative revenue) | | `checkout.session.completed` | Checkout Completed | Checkout with Tier 1 attribution | | `invoice.paid` | Invoice Paid | Recurring subscription payment | | `customer.subscription.created` | Subscription Updated | New subscription | | `customer.subscription.updated` | Subscription Updated | Plan change / upgrade | | `customer.subscription.deleted` | Subscription Canceled | Subscription cancellation | ## Historical Backfill When you first connect Stripe, Ripples automatically imports all your historical charges and attributes them using the same 3-tier fallback. Active subscriptions are also synced to update visitor plan fields. You can check the backfill status on the Integrations settings page. If needed, use the **Re-sync** button to clear and re-import all data. ## Deduplication Ripples automatically prevents duplicate revenue events: - Each Stripe charge, invoice, and checkout session is tracked by its unique ID - Webhook retries and backfill re-runs are automatically deduplicated - If you use the Stripe integration, you do **not** need to send revenue events from the frontend SDK — Stripe handles it automatically ## Disconnecting To disconnect the integration, go to Integrations settings and click **Disconnect** on the Stripe card. You can choose to keep or remove the imported revenue data. --- # Paddle Integration Automatically track revenue from Paddle and attribute it to the visitors who converted. ## Overview The Paddle integration connects your Paddle account to Ripples so you can: - See revenue attributed to each visitor and their acquisition source - Track transactions, subscriptions, and adjustments automatically - Backfill historical payment data when you first connect - Handle refunds and credits (negative revenue events are created automatically) Revenue events appear in the visitor timeline and contribute to each profile's lifetime revenue total. ## Prerequisites Before connecting Paddle, make sure you have: 1. **Persist mode enabled** — Ripples uses a cookie (`_rpl_vid`) to track visitors across sessions. This is the default behavior. 2. **User identification** — Call `ripples.identify()` when users sign in so we can match Paddle transactions to visitor profiles by email. ```javascript // Call this after login / signup ripples.identify({ id: "user_123", email: "jane@example.com", name: "Jane Doe", plan: "pro", }) ``` ## Create an API Key 1. Go to your [Paddle Dashboard → Developer Tools → Authentication](https://vendors.paddle.com/authentication) 2. Click **"Generate API key"** 3. Give it a name like `Ripples Analytics` 4. Set the following permissions: | Permission | Access | | --- | --- | | Notification settings | Read & Write | | Transactions | Read | | Subscriptions | Read | | Customers | Read | | Products | Read | The **Notification settings: Read & Write** permission allows Ripples to automatically create a webhook notification destination in your Paddle account to receive payment events in real-time. 5. Click **"Generate"** and copy the key (it starts with `pdl_`) ## Connect in Settings 1. Open your project dashboard in Ripples 2. Click the **Integrations** link in the header 3. Find the **Paddle** card and click **Connect** 4. Paste your API key and click **Connect** Once connected, Ripples will: - Create a notification destination in your Paddle account automatically - Begin importing your historical transactions (backfill) - Start receiving new payment events in real-time ## Revenue Attribution Ripples uses a **3-tier fallback** to attribute each Paddle payment to a visitor: ### Tier 1: Custom Data (Best) Pass the visitor ID in the `custom_data` field when creating a Paddle checkout or transaction. This gives the most accurate attribution because it directly links the payment to the browsing session. ```javascript // Paddle.js (Client-side) const visitorId = ripples.getVisitorId() Paddle.Checkout.open({ items: [{ priceId: "pri_..." }], customData: { ripples_visitor_id: visitorId }, }) ``` ```javascript // Next.js / Node.js (Server-side) import { cookies } from "next/headers" export async function createCheckout() { const cookieStore = await cookies() const visitorId = cookieStore.get("_rpl_vid")?.value // Pass to Paddle API when creating a transaction const transaction = await paddle.transactions.create({ items: [{ priceId: "pri_...", quantity: 1 }], customData: { ripples_visitor_id: visitorId }, }) } ``` ```php // Laravel (Server-side) $visitorId = $request->cookie('_rpl_vid'); // or from frontend: $request->input('visitorId') // Pass to Paddle API when creating a transaction $paddle->transactions->create([ 'items' => [['price_id' => 'pri_...', 'quantity' => 1]], 'custom_data' => ['ripples_visitor_id' => $visitorId], ]); ``` ### Tier 2: Email Matching (Automatic) If Tier 1 custom data is not present, Ripples matches the Paddle customer email to a visitor profile. This works when you have called `ripples.identify()` with the user's email. No extra setup needed — this happens automatically. ### Tier 3: Customer ID (Returning Customers) For subsequent payments, Ripples remembers the Paddle customer ID from previous attributions. If a customer was attributed once (via Tier 1 or 2), all future payments from that customer are automatically attributed to the same visitor. ## Events Tracked The following Paddle events are processed: | Paddle Event | Ripples Event | Description | | --- | --- | --- | | `transaction.completed` | Transaction Completed | Successful payment | | `adjustment.created` | Refund / Credit | Refund or credit (negative revenue) | | `subscription.created` | Subscription Updated | New subscription | | `subscription.updated` | Subscription Updated | Plan change / upgrade | | `subscription.canceled` | Subscription Canceled | Subscription cancellation | | `subscription.past_due` | Subscription Updated | Payment failed, subscription past due | ## Historical Backfill When you first connect Paddle, Ripples automatically imports all your historical completed transactions and attributes them using the same 3-tier fallback. Active subscriptions are also synced to update visitor plan fields. You can check the backfill status on the Integrations settings page. If needed, use the **Re-sync** button to clear and re-import all data. ## Deduplication Ripples automatically prevents duplicate revenue events: - Each Paddle transaction and adjustment is tracked by its unique ID - Webhook retries and backfill re-runs are automatically deduplicated - If you use the Paddle integration, you do **not** need to send revenue events from the frontend SDK — Paddle handles it automatically ## Sandbox Support Ripples automatically detects Paddle sandbox API keys (containing `test_` or `sandbox` in the key) and routes requests to the Paddle sandbox environment. You can test the full integration flow without processing real payments. ## Disconnecting To disconnect the integration, go to Integrations settings and click **Disconnect** on the Paddle card. You can choose to keep or remove the imported revenue data. The notification destination in your Paddle account will be automatically removed. --- # PHP SDK Track revenue, signups, product usage, and user identity from your PHP backend using the official Ripples SDK. > Server-side tracking. Use the PHP SDK to send events from your backend — ideal for tracking payments processed server-side, webhooks, or any event that shouldn't go through the browser. ## Install ```bash composer require ripplesanalytics/ripples-php ``` Add your secret key to your `.env`: ``` RIPPLES_SECRET_KEY=priv_your_secret_key ``` Your secret key can be found in your Ripples dashboard under **Settings → API Keys**. ## Quick start ```php use Ripples\Ripples; $ripples = new Ripples(); // Track a payment $ripples->revenue(49.99, 'user_123'); // Track a signup $ripples->signup('user_123', ['email' => 'jane@example.com']); // Track product usage $ripples->track('created a budget', 'user_123', ['area' => 'budgets']); // Identify / update a user $ripples->identify('user_123', ['email' => 'jane@example.com']); ``` ## Track revenue Call `revenue()` whenever a payment is processed on your server: ```php $ripples->revenue(49.99, 'user_123'); ``` Pass extra context as a third argument. Any key that isn't a known field becomes a custom property automatically: ```php $ripples->revenue(49.99, 'user_123', [ 'email' => 'jane@example.com', 'currency' => 'EUR', 'transaction_id' => 'txn_abc123', 'name' => 'Pro Plan', 'plan' => 'annual', // custom property 'coupon' => 'WELCOME20', // custom property ]); ``` Refunds are negative revenue: ```php $ripples->revenue(-29.99, 'user_123', ['transaction_id' => 'txn_abc123']); ``` ### Parameters | Parameter | Type | Description | | --- | --- | --- | | `amount` (required) | `float` | Revenue amount in your default currency. Use negative values for refunds. | | `user_id` (required) | `string` | Your internal user ID. | | `properties` (optional) | `array` | Additional properties: `email`, `currency`, `transaction_id`, `name`, plus any custom keys. | ## Track product usage Call `track()` when a user does something meaningful in your product. This powers the **Activation** dashboard — showing which product areas drive engagement and conversion. ```php // User did something meaningful $ripples->track('created a budget', 'user_123', [ 'area' => 'budgets', // optional: group into a product area ]); // User added their 10th transaction — we consider this their activation moment $ripples->track('added transaction', 'user_123', [ 'area' => 'transactions', 'activated' => true, // marks THIS occurrence as the activation moment, not every "added transaction" ]); ``` > How it works. Ripples auto-detects activation moments (first occurrence per user per action), computes adoption rates by product area, and correlates usage patterns with retention and payment. ### Parameters | Parameter | Type | Description | | --- | --- | --- | | `actionName` (required) | `string` | What the user did. Be specific: `'created a budget'`, not `'budgets'`. | | `userId` (required) | `string` | Your internal user ID. | | `area` (optional) | `string` | Product area this action belongs to (e.g. `'budgets'`, `'reports'`). Groups actions in the dashboard. | | `activated` (optional) | `bool` | Set to `true` on the specific occurrence when activation happens for this user. This does not mark the event type as an activation event — it marks this particular moment as when the user activated. For example, `'sent message'` is a regular event, but when a user sends their 10th message you may consider that their activation moment and send that occurrence with `activated => true`. | ## Track signups Call `signup()` when a new user registers: ```php $ripples->signup('user_123', [ 'email' => 'jane@example.com', 'name' => 'Jane Smith', 'referral' => 'twitter', // custom property 'plan' => 'free', // custom property ]); ``` ### Laravel example ```php use Ripples\Ripples; class AuthController extends Controller { public function register(Request $request) { $user = User::create([...]); $ripples = new Ripples(); $ripples->signup($user->id, [ 'email' => $user->email, 'name' => $user->name, 'ripples_visitor_id' => $request->cookie('_rpl_vid'), ]); return $user; } } ``` > Tip: pass the visitor ID. Include ripples_visitor_id with the value from the _rpl_vid cookie to link this signup back to the anonymous browsing session and correct traffic source attribution. ## Identify users Call `identify()` to update user traits at any time — on login, plan change, profile update, etc.: ```php $ripples->identify('user_123', [ 'email' => 'jane@example.com', 'name' => 'Jane Smith', 'avatar_url' => 'https://example.com/avatars/jane.jpg', 'company' => 'Acme Inc', // custom property 'role' => 'admin', // custom property ]); ``` ### Properties | Key | Type | Description | | --- | --- | --- | | `email` (optional) | `string` | User's email address. Shown in the visitor profile and used for Stripe revenue attribution. | | `name` (optional) | `string` | Display name shown in the visitor profile. | | `avatar_url` (optional) | `string` | URL to the user's avatar image. Displayed in the visitor profile. Also accepted as `avatar`. | | `[custom]` (optional) | `string | number` | Any additional traits you want to attach to the user (e.g. `plan`, `company`, `role`). | ## Error handling ```php use Ripples\RipplesException; try { $ripples->revenue(49.99, 'user_123'); } catch (RipplesException $e) { // Log or handle gracefully — never block the user flow Log::warning('Ripples error: ' . $e->getMessage()); } ``` ## Configuration The SDK reads `RIPPLES_SECRET_KEY` from your environment automatically. You can also pass it explicitly: ```php $ripples = new Ripples('priv_explicit_key', [ 'base_url' => 'https://your-domain.com/api', // self-hosted 'timeout' => 10, // seconds (default: 5) ]); ``` The self-hosted URL can also be set via env: ``` RIPPLES_URL=https://your-domain.com/api ``` ### Configuration options | Option | Default | Description | | --- | --- | --- | | `base_url` | `https://api.ripples.sh` | API endpoint. Override for self-hosted deployments. | | `timeout` | `5` | HTTP request timeout in seconds. | ## Custom HTTP client Extend the class and override `post()` to use Guzzle, Symfony HttpClient, or any other HTTP library: ```php class MyRipples extends \Ripples\Ripples { protected function post(string $path, array $data): array { // your custom implementation using Guzzle, etc. } } ``` ## Requirements - PHP 8.1+ - `ext-curl` - `ext-json`