Library / SDK
panphp/pan avatar
panphp/pan

Pan: one HTML attribute for product analytics on Laravel

A simple, lightweight, and privacy-focused product analytics php package

1,272 stars63 forksPHPMIT

At a glance

What is it?
Pan is an MIT-licensed PHP package that records impressions, hovers and clicks by watching for a `data-pan` attribute, stores nothing but a name and a counter, and shows you the result in an Artisan command. The judgement is that its narrowness is the feature, and that its three event types and its default cap of 50 records mean it answers a specific question rather than replacing an analytics platform.
Who is it for?
Pan fits a Laravel developer who has one specific question, which tab or button on a page is actually being used, and who wants the answer without shipping a third-party analytics script or collecting anything about the visitor. It does not fit a team that needs a funnel, retention cohorts, session replay or attribution, because it tracks exactly three events and stores only a counter.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Activity is slowing. The repository last received commits 6 months ago.
What is it written in?
Mainly PHP, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A data-pan attribute and three event types

Pan is described as a lightweight and privacy-focused PHP product analytics library, designed as a very simple package that you install via `composer require` and start tracking your pages or components with a simple `data-pan` attribute.

The scope of what it tracks is stated up front, and it is worth reading before anything else. At the time of writing, Pan tracks only the following events: impressions, hovers, and clicks. It does not collect any personal information, such as IP addresses, user agents, or any data that could be used to identify a user.

Three event types and a name-and-counter storage model. That is the entire product, and the examples show why it is enough for the cases it targets.

The first case is different tabs within a page with the same URL, where you want to know which one is the most viewed. The second is different register buttons, where you want to know which one is the most clicked. The third is different help pop-hovers, where you want to know which one is the most hovered. In each case, the answer is a count per name, and that is exactly what Pan stores.

The privacy claim is not a marketing position bolted onto a tracking library, it is a consequence of the data model. The server side stores the analytic name and a counter of how many times the different events were triggered. There is no session, no user identifier and no timestamp of the individual event, so there is nothing to join against other data later. That is a genuinely different posture from a hosted analytics tool, and it is the reason the package calls itself privacy-focused.

The primary language is PHP, the licence is MIT with a `LICENSE.md` at the repository root, and the repository is not archived.

Installation is two commands and an attribute

The requirements are strict and stated at the top of the getting started section: PHP 8.3 or later, and Laravel 11.0 or later.

The install is a Composer require followed by an Artisan command:

bash
composer require panphp/pan
bash
php artisan install:pan

Then you add the attribute to the elements you care about:

diff
<div>
-    <button>Tab 1</button>
+    <button data-pan="tab-1">Tab 1</button>
-    <button>Tab 2</button>
+    <button data-pan="tab-2">Tab 2</button>
</div>

The diff form is a good choice for a README because it makes the change size obvious. In a real application you would be adding the attribute to a Blade template, a React component or a Livewire view, and the value is the event name you will later query by.

There is one naming rule, and it is marked as important: event names must only contain letters, numbers, dashes, and underscores. That constraint exists because the name is the primary key in the data model, and a free-form string would be awkward to store, index and filter.

The package also fits into the modern Laravel stack without special handling. It works out-of-the-box with the favourite Laravel stack, and the documentation calls out a specific behaviour worth knowing: updating a button colour in your React app will not trigger a new impression, but seeing that same button in a different Inertia page will. Inertia swaps the page without a full reload, and Livewire is said to work with it too.

So the tracking is tied to elements actually being rendered, which means a single-page navigation can count an impression for the same element if it is genuinely re-rendered into a new page, but not for a cosmetic change.

The Filament integration covers one action or all of them

Pan has a specific integration for Filament, and it comes in two forms that answer different questions.

The first tracks a single action by passing the `data-pan` attribute through `extraAttributes`:

php
Action::make('subscribe')
    ->extraAttributes(['data-pan' => 'subscribe-button'])

The second tracks all actions globally, by configuring the action class in a service provider's `boot` method:

php
use Filament\Actions\Action;

public function boot(): void
{
    Action::configureUsing(function (Action $action): void {
        $action->extraAttributes(fn () => ['data-pan' => $action->getLabel()]);
    });
}

The global version derives the event name from the action's label, which is convenient and has a consequence worth noting: the name is then whatever the label happens to be, and labels are display strings that can change. If a label is renamed, the previously recorded counts are keyed on a name that no longer appears.

The Filament integration is worth a mention in the wider picture of the package, because it is the clearest case of the library being useful precisely where a general analytics tool is too heavy. A Filament admin panel has dozens of actions, and knowing which one gets used is a question about a handful of counters, not about user journeys.

The same is true of the Artisan command, which is the whole reporting surface:

bash
php artisan pan
php artisan pan --filter=tab-profile

A table of what has been tracked, with a filter for a specific event name. There is no dashboard, no charting and no export in the documentation, and that is a design decision rather than a gap in the current build.

The 50-record cap exists because your HTML is editable

The security model of Pan is small but real, and the documentation is unusually direct about it.

By default, Pan tracks all the HTML elements with the `data-pan` attribute, so bad actors could alter your HTML and create unwanted analytics records in your database. To mitigate this, by default, Pan only allows 50 analytics records to be created.

That is the whole threat model: since the event name arrives from the client, anyone who can change the page can invent new event names and fill your database with rows. The default cap of 50 distinct records bounds that damage, since the attack is about creating many names rather than inflating one counter.

For a stricter approach, the `PanConfiguration::allowedAnalytics` method whitelists the analytics you want to track, so only the whitelisted ones are stored:

php
use Pan\PanConfiguration;

public function register(): void
{
    PanConfiguration::allowedAnalytics([
        'tab-profile',
        'tab-settings',
    ]);
}

For a looser one, `PanConfiguration::maxAnalytics(10000)` raises the cap on the number of analytics records created, and `PanConfiguration::unlimitedAnalytics()` removes the limit entirely.

The three configuration methods form a sensible ladder: whitelist what you expect, raise the cap if your legitimate event names vary, or remove the cap if you accept the risk. The default of 50 is the interesting one, because it is low enough that a page with more than 50 distinct tracked elements will start failing, and the documentation does not say what happens in that case, only that the records are limited.

One more configuration option controls where the data goes. By default, Pan's route prefix is `/pan`, and `PanConfiguration::routePrefix('internal-analytics')` changes it, with the tracking URL becoming `/internal-analytics/events`. Moving that endpoint off a well-known path is a reasonable thing to do for a public-facing application.

Middleware, a small injected script and batched requests

The How does it work section describes the mechanism in enough detail to reason about its behaviour.

Via middleware, Pan injects a simple JavaScript library into your HTML pages. This library listens to events like `viewed`, `clicked`, or `hovered` and sends the data to your Laravel application.

Two details matter for a production deployment. First, the client-side library collects events in a very performant way and batches them together to reduce the number of requests to your server. A page with twenty tracked elements produces one request, not twenty, which is the difference between a tracking library you can ship and one you cannot.

Second, the server side stores only the analytic name and a counter of how many times the different events were triggered. No payload, no event timestamp, no session token. The trade-off is direct and worth stating plainly: you get counts, and only counts. You cannot answer when a tab was viewed, how many times one user viewed it, or whether the same person saw two different variants of a page, because none of that is recorded.

That constraint is the design, not an oversight. The privacy property and the analytic power are the same decision taken twice.

The maintenance commands are straightforward. Flushing the data is `php artisan pan:flush`, and deleting a specific analytic record by its ID is `php artisan pan:delete <id>`.

The repository layout shows a package with a PHP side and a small TypeScript side. There is `src/` for the PHP code, `resources/` for the client-side assets, `database/` for the migrations, `docs/` for the documentation, `tests/` for the PHP tests, and `package.json` with `vite.config.ts` and `tsconfig.json` at the root for the client bundle. The `package.json` scripts are `tsc` for type checking, `dev` as `vite build --watch`, `build` as `tsc && vite build`, and `lint` and `test:lint` running prettier over `resources/js/src`.

What the package will not tell you, and the state of the repository

Two things to weigh before adopting.

The first is the analytic ceiling. Three event types and a counter per name means Pan can answer which of my tabs is most viewed. It cannot answer how many unique visitors a page had, where they came from, what they did next, or whether a change to a button improved anything. If the question you have needs any of that, this is the wrong tool, and no amount of configuration adds it.

The second is that the counters are global, not per session. Two hundred people clicking the button and one person clicking it two hundred times produce the same number. For a UI question like which of several help pop-hovers is used, that is fine. For anything that resembles a funnel, it is not.

The repository state is the other thing to note. The last push was on 2026-04-12, which is more than six months before today, and the repository is not archived. The project has no GitHub releases, and the version comes from Packagist rather than from tags. The README itself is a fair indicator of a package in a stable, focused state rather than one in active development: the feature list is short, the API surface is small, and the CHANGELOG is present at the repository root.

The code quality tooling suggests a maintained package. There is a `phpstan.neon.dist` for static analysis, `phpunit.xml.dist` for the test suite, `rector.php` for automated refactoring, a `composer.json` alongside a `package.json`, a `vite.config.ts` for the client bundle, and a GitHub Actions workflow with a tests badge in the README header. The package is also on Packagist under `panphp/pan` with download and version badges.

The MIT licence is a permissive grant with no copyleft obligation, which for a package that stores data in your own database and injects a script into your own pages is the straightforward choice. That is a licensing observation rather than legal advice, and the `LICENSE.md` file governs.

Editorial conclusion

Pan fits a Laravel developer who has one specific question, which tab or button on a page is actually being used, and who wants the answer without shipping a third-party analytics script or collecting anything about the visitor. It does not fit a team that needs a funnel, retention cohorts, session replay or attribution, because it tracks exactly three events and stores only a counter. Verify first that the events you care about map onto impressions, hovers and clicks, that the default limit of 50 analytics records is raised or whitelisted before you go to production, since a bad actor can otherwise create records in your database, and that the library version you install matches the Laravel and PHP versions your application runs, since the package requires PHP 8.3 and Laravel 11 or newer.

Frequently asked questions

What events does the Pan PHP package track?

Only three: impressions, hovers and clicks. It does not collect personal information such as IP addresses, user agents, or any data that could be used to identify a user, and on the server side it stores only the analytic name and a counter of how many times each event was triggered.

How do I install Pan and start tracking elements?

Require it with `composer require panphp/pan`, run `php artisan install:pan`, then add the `data-pan` attribute to the elements you want tracked. Event names must only contain letters, numbers, dashes and underscores. PHP 8.3 or later and Laravel 11.0 or later are required.

How do I prevent Pan from storing unwanted analytics records?

By default Pan only allows 50 analytics records to be created, because bad actors could alter your HTML and create records in your database. You can whitelist the events you expect with `PanConfiguration::allowedAnalytics`, raise the cap with `PanConfiguration::maxAnalytics(10000)`, or remove it with `PanConfiguration::unlimitedAnalytics()`.

How do I view and manage Pan analytics in a Laravel app?

Run `php artisan pan` to see a table of what has been tracked, with `php artisan pan --filter=tab-profile` for a specific event. `php artisan pan:flush` clears the data and `php artisan pan:delete <id>` removes a single record by its ID.

Does Pan work with Inertia, Livewire and Filament?

The documentation says it works out-of-the-box with the favourite Laravel stack, works with Inertia and Livewire, and integrates with Filament through `extraAttributes`, either per action or globally for all actions via `Action::configureUsing` in a service provider.

Official sources

  1. Issues
  2. License: MIT
  3. panphp/pan on GitHub
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/panphp-pan.svg)](https://hysenlabs.com/projects/panphp-pan)

Was this useful?