
# PHP event tracking

Event tracking is **on by default** in the PHP SDK (`events.enabled`, environment variable `RANETRACE_EVENTS_ENABLED`). Use it to record things that matter to your business from anywhere in your application.

Using Laravel? See [Event Tracking](/docs/event-tracking) instead.

## Tracking an event

```php
$ranetrace->trackEvent('button_clicked', [
    'button_id' => 'header-cta',
    'page' => 'homepage',
]);
```

The full signature:

```php
$ranetrace->trackEvent(
    string $name,
    array $properties = [],
    int|string|null $userId = null,
    bool $validate = true,
);
```

Properties are free-form. They are sanitized for serialization and secret-scrubbed before they leave your host, so a value under a key like `api_token` is redacted rather than sent.

## Naming rules

An event name must be 3 to 50 characters, start with a letter, and contain only lowercase letters, digits and underscores. In other words, snake_case.

An invalid name is a mistake you can still fix, so it is the one thing in the capture path that fails loudly: `trackEvent()` throws an `InvalidArgumentException` before any capture work happens. Everything after that point is caught and dropped instead, because monitoring must never be the reason a checkout breaks.

If you have to preserve a name from an external system that breaks the convention, pass `validate: false` or use `customUnsafe()`. Reach for it rarely: unconventional names make the events dashboard harder to read for everyone after you.

## Convenience methods

`events()` returns the event tracker, which has typed methods for the events most applications record:

```php
$events = $ranetrace->events();

// Sales
$events->sale(
    orderId: 'ORDER-456',
    totalAmount: 89.97,
    products: [
        ['id' => 'PROD-123', 'name' => 'Widget', 'price' => 29.99, 'quantity' => 3],
    ],
    currency: 'USD',
);

// Cart additions
$events->productAddedToCart(
    productId: 'PROD-123',
    productName: 'Widget',
    price: 29.99,
    quantity: 1,
    category: 'tools',
);

// Authentication
$events->userRegistered(userId: $user->id);
$events->userLoggedIn(userId: $user->id);

// Page views, recorded server side
$events->pageView(pageName: 'pricing');

// Any other event, name validated
$events->custom('newsletter_signup', ['source' => 'footer']);

// Same, but skipping name validation
$events->customUnsafe('Legacy.Signup');
```

Every method takes an `array $additionalProperties = []` as its last argument for ad-hoc fields. Those are merged over the properties the method builds, so you can override any of them. The one exception is `productAddedToCart()`'s `category`, which is applied after the merge: the named argument is the more specific statement of intent, so it wins.

What each method fills in for you:

| Method | Event name | Properties it sets |
|---|---|---|
| `sale()` | `sale` | `order_id`, `total_amount`, `currency`, `products`, `product_count` |
| `productAddedToCart()` | `product_added_to_cart` | `product_id`, `product_name`, `price`, `quantity`, `total_value`, `category` |
| `userRegistered()` | `user_registered` | none, your properties are sent as given |
| `userLoggedIn()` | `user_logged_in` | none, your properties are sent as given |
| `pageView()` | `page_view` | `page_name` |
| `custom()` | your name | none |
| `customUnsafe()` | your name, unvalidated | none |

## The privacy model

Every event carries the name, the properties, a timestamp, and the URL of the current request. The URL has its sensitive query parameters redacted, and under CLI there is no URL, so the field is empty.

Two identifiers travel with an event, and both are one-way hashes:

- **`user_agent_hash`**, an HMAC-SHA256 of the raw user agent. Empty when the request carried none, so "no user agent" stays distinguishable from "some user agent we hashed".
- **`session_id_hash`**, an HMAC-SHA256 of the client IP, the first 100 characters of the user agent, and today's date. Because the date is part of the input, this fingerprint rotates every day, and it cannot be joined across sites.

The raw IP address is never part of the payload. Neither is an email: the only user field an event carries is the id.

That id comes from the `$userId` argument when you pass one, and otherwise from the `user_resolver` you configured (see [PHP error tracking](/docs/php-error-tracking)). When there is neither, the event is recorded without a user.

### The fingerprint salt

The hashes are salted per install. By default the salt is your API key, which every install already has and which never travels in a payload, so fingerprints are non-reversible out of the box.

Set an explicit salt when you want to rotate fingerprints independently of the key, for example after a key rotation that should not break event grouping:

```dotenv
RANETRACE_FINGERPRINT_SALT=a-long-random-string
```

Changing the salt changes every hash, so past and future events for the same visitor stop lining up. That is the point when you rotate deliberately, and worth knowing before you change it by accident.

## Configuration

| Environment variable | Config key | Default | What it controls |
|---|---|---|---|
| `RANETRACE_EVENTS_ENABLED` | `events.enabled` | `true` | Whether events are captured at all. |
| `RANETRACE_EVENTS_TIMEOUT` | `events.timeout` | `10` | Seconds to wait for the API when a batch of events is sent. |
| `RANETRACE_FINGERPRINT_SALT` | `fingerprint_salt` | your API key | Salt for the user agent and session hashes. |

To turn event tracking off:

```dotenv
RANETRACE_EVENTS_ENABLED=false
```

Captured events reach the API on the next flush. See [PHP installation](/docs/php-installation).
