Search by

ripplesanalytics / ripples-php

zhavoronkov

Official PHP SDK for Ripples Metrics (ripples.sh): server-side event tracking

Package info

github.com/RipplesMetrics/ripples-php

Homepage

pkg:composer/ripplesanalytics/ripples-php

Statistics

Installs: 99

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.10 2026-10-11 14:18 UTC

This package is auto-updated.

Last update: 2026-10-11 14:19:15 UTC


README

Server-side PHP SDK for Ripples Metrics analytics.

Install

composer require ripples/ripples-php

Add to your .env:

RIPPLES_SECRET_KEY=priv_your_secret_key

Usage

use Ripples\Ripples;

$ripples = new Ripples();

$ripples->revenue(49.99, 'user_123');
$ripples->signup('user_123', ['email' => 'jane@example.com']);
$ripples->track('created a budget', 'user_123', ['area' => 'budgets']);
$ripples->identify('user_123', [
    'email' => 'jane@example.com',
    'signed_up_at' => $user->created_at->toIso8601String(),
]);

That's it.

Track product usage

Call track() only for significant product usage — actions that prove a user got real value (created a budget, sent a message, invited a teammate). This is not a generic event log like PostHog or Mixpanel: do not send pageviews, banner impressions, button clicks, or "viewed X" events. Every track() call feeds the Activation dashboard, so noise here pollutes your funnel. Ripples auto-detects activation (first occurrence per user), computes adoption rates, and correlates with retention and payment.

$ripples->track('created a budget', 'user_123', ['area' => 'budgets']);
$ripples->track('shared a list', 'user_123', ['area' => 'sharing', 'via' => 'link']);
$ripples->track('exported report', 'user_123', ['area' => 'reports', 'format' => 'csv']);

Use area to group actions into product areas. Use activated => true to mark the specific moment a user activates — it flags this occurrence, not the event type:

// User added their 10th transaction — we consider this their activation moment
$ripples->track('added transaction', 'user_123', [
    'area'      => 'transactions',
    'activated' => true,  // only on THIS occurrence
]);

Milestones

A milestone is a moment in a person's lifecycle that you count once per person, date by when they first reached it, and attribute to how they were acquired: mql, sql, demo_requested, demo_held, onboarding_completed, paywall_viewed. It is not product usage, so it never feeds activation, retention or "last seen" the way track() does.

Would you put it on a funnel next to signed up and paid, and count each person once? → milestone(). Do people do it repeatedly as part of using the product? → track().

// A known user: date it by when it happened (the CRM's stage-change time)
$ripples->milestone('mql', userId: '42', properties: ['source' => 'hubspot'], timestamp: $changedAt);

// A lead with no account yet: by the browser cookie (automatic under PHP-FPM) or by email
$ripples->milestone('demo_requested', email: $form->email, properties: ['company_size' => '50-200']);
  • Who it belongs to, strongest first: userId, then the browser visitor (the tracker cookie, setVisitorId() or visitorId:), then email. A milestone naming nobody is not sent and is reported to on_error.
  • A lead known by email is merged into the account when you later identify() the user with the same email. Send an email only once it is verified: it merges that lead's history into whoever owns the address.
  • Names are snake_case. Use the recommended names where they fit (lead_captured, demo_requested, demo_booked, demo_held, mql, sql, email_verified, onboarding_completed, integration_connected, teammate_invited, teammate_joined, invite_sent, invite_accepted, paywall_viewed, checkout_started). Built-in moments (signed_up, activated, first_paid, …) and billing events (payment_failed, refunds, renewals) are refused.
  • Sending one again is fine (CRM webhooks re-fire). Reports count each person once, on the day of their first.
  • In a request that is not the person's own (an admin marking a user as SQL), the tracker cookie belongs to the admin. Send it from a queue job instead (no cookie is read under the CLI).

HTTP shape (POST /v1/ingest/batch):

{"$type": "milestone", "$name": "mql", "$user_id": "42", "$sent_at": "2026-09-14T10:00:00Z", "$properties": {"source": "hubspot"}}

Docs: https://ripples.sh/docs/milestones

Track subscriptions (MRR)

Call subscription() when a subscription is created, upgraded, downgraded, or canceled. This powers the MRR metric on your dashboard.

Stripe / Paddle users: MRR is tracked automatically via the integration. Only use this method if you use a payment provider without a native Ripples integration.

// User subscribes to Pro Monthly ($29/mo)
$ripples->subscription('sub_123', 'user_456', 'active', 29.00, 'month', [
    'name' => 'Pro',
    'currency' => 'EUR',
]);

// User upgrades to Business Annual ($499/yr)
$ripples->subscription('sub_123', 'user_456', 'active', 499.00, 'year', [
    'name' => 'Business',
]);

// User turns auto-renew off: NOT a cancel yet, they are paid until the period ends
$ripples->subscription('sub_123', 'user_456', 'active', 499.00, 'year', [
    'renews'         => false,
    'period_ends_at' => new DateTimeImmutable('2027-01-05T00:00:00Z'),
]);

// The subscription has ended
$ripples->subscription('sub_123', 'user_456', 'canceled', 0);

canceled means the subscription has ended: MRR drops and churn is dated. Auto-renew off (the App Store and Play switch, Stripe's cancel_at_period_end) is the live status with renews => false. A trial is trialing at the price it will bill at. See Auto-renew off is not a cancel for the Apple and Play mapping.

Parameters:

  • subscriptionId (string, required) — stable identifier for the subscription
  • userId (string, required) — your internal user ID
  • status (string, required) — one of: active, canceled, past_due, trialing, paused
  • amount (float, required) — amount per billing cycle (e.g. 29.00), pass 0 when canceling
  • interval (string, optional) — month (default), year, week, or day
  • attributes (array, optional) — currency, name or plan, interval_count, renews (bool), period_ends_at

Track revenue

$ripples->revenue(49.99, 'user_123');

Any key you pass that isn't a known field becomes a custom property automatically:

$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 just negative revenue:

$ripples->revenue(-29.99, 'user_123', ['transaction_id' => 'txn_abc123']);

Track signups

$ripples->signup('user_123', [
    'email'    => 'jane@example.com',
    'name'     => 'Jane Smith',
    'referral' => 'twitter',    // custom property
    'plan'     => 'free',       // custom property
]);

Identify users

Update user traits at any time:

$ripples->identify('user_123', [
    'email'        => 'jane@example.com',
    'name'         => 'Jane Smith',
    'signed_up_at' => $user->created_at->toIso8601String(),
    'company'      => 'Acme Inc',   // custom property
    'role'         => 'admin',      // custom property
]);

Always pass signed_up_at from server-side calls. identify() can be the first time Ripples ever hears about a user — a cron job, a login listener, a user who never went through a client-side signup. When that happens, Ripples treats the call as a brand-new signup; without signed_up_at it dates that signup "now" instead of the user's real account age, which corrupts your signups chart and retention cohorts. It only ever moves the stored signup date earlier, never later, so there's no downside to sending it on every call.

Track companies (B2B)

Say which company a user works in, at signup and whenever they switch or the company's traits change. Their later events count for it with nothing passed per call:

$ripples->group($user->id, $team->id, ['name' => $team->name, 'plan' => 'business']);

$ripples->track('created a report', $user->id);   // counts for $team

Use your own id for the company, never its name. Docs: https://ripples.sh/docs/companies

Backfill historical events

Pass a DateTimeInterface as the last argument to any tracking method to override the event's timestamp — useful when importing from a CSV, replaying from another analytics tool, or catching up after an outage.

foreach ($csvRows as $row) {
    $ripples->track(
        $row['action'],
        $row['user_id'],
        ['area' => $row['area']],
        new DateTimeImmutable($row['occurred_at']), // any tz — converted to UTC
    );
}
$ripples->flush(); // guarantee delivery at end of script

Naive behavior (no timestamp passed) uses "now" in UTC. Non-UTC timezones are converted automatically.

Error handling

use Ripples\RipplesException;

try {
    $ripples->revenue(49.99, 'user_123');
} catch (RipplesException $e) {
    // handle error
}

Configuration

The SDK reads RIPPLES_SECRET_KEY from your environment automatically. You can override everything:

$ripples = new Ripples('priv_explicit_key', [
    'base_url' => 'https://your-domain.com/api', // self-hosted
    'timeout'  => 10, // seconds (default: 5)
]);

Self-hosted URL can also be set via env:

RIPPLES_URL=https://your-domain.com/api

Custom HTTP client

Extend the class and override post() to use Guzzle, Symfony HTTP, or anything else:

class MyRipples extends \Ripples\Ripples
{
    protected function post(string $path, array $data): array
    {
        // your custom implementation
    }
}

Requirements

  • PHP 8.1+
  • ext-curl
  • ext-json

License

MIT