bahdan / symfony-privacy-analytics-bundle
Zero-cookie, GDPR-compliant server-side privacy analytics bundle for Symfony
Package info
github.com/bahdanhal/symfony-privacy-analytics-bundle
Type:symfony-bundle
pkg:composer/bahdan/symfony-privacy-analytics-bundle
Requires
- php: >=8.3
- symfony/cache-contracts: ^3.0
- symfony/config: ^7.0 || ^8.0
- symfony/dependency-injection: ^7.0 || ^8.0
- symfony/event-dispatcher: ^7.0 || ^8.0
- symfony/http-foundation: ^7.0 || ^8.0
- symfony/http-kernel: ^7.0 || ^8.0
Requires (Dev)
- doctrine/dbal: ^3.8 || ^4.0
- doctrine/orm: ^3.0
- phpunit/phpunit: ^11.0 || ^12.0 || ^13.0
- symfony/cache: ^7.0 || ^8.0
Suggests
- doctrine/dbal: Required when using Doctrine storage.
- doctrine/orm: Required when using Doctrine storage.
- symfony/messenger: Required for asynchronous analytics ingestion.
Provides
None
Conflicts
None
Replaces
None
README
A lightweight, zero-cookie, GDPR-compliant server-side web analytics bundle for Symfony applications.
Used in production by Stackhal, IleZa.pl and bahdanhal.pl.
Key Features
- Zero Cookies / No Consent Banner Needed: Anonymously hashes
IP + User-Agent + secretwith HMAC-SHA256. - Bot & Crawler Filtering: Ignores automated crawlers, spiders, and testing clients automatically.
- Privacy Header Compliance: Honors
DNT(Do Not Track) andSec-GPC(Global Privacy Control) request headers. - Referrer Categorization: Automatically groups incoming traffic into
search,social,referral,internal, anddirect. A visit without a referrer is attributed to the hostname inutm_sourcewhen there is one (for exampleutm_source=chatgpt.com); otherutm_sourcevalues and the rest of the query string are ignored. - Dual Storage: Supports Doctrine ORM (PostgreSQL/SQLite/MySQL) and monthly partitioned JSONL storage with streaming retention pruning.
- SQL Aggregation: Doctrine summaries aggregate counts, unique visitors, sources, referrers, paths, and daily metrics in the database rather than hydrating raw page views.
- Cached Summaries: Dashboard aggregates use the application's PSR-6-compatible Symfony cache for a configurable short TTL.
- Optional Async Ingestion: Symfony Messenger can move writes off the request worker for traffic spikes.
- Extensible Bot Filtering: Add project-specific user-agent substrings without forking the bundle.
Installation
composer require bahdan/symfony-privacy-analytics-bundle
Doctrine is optional. Install it only when selecting the Doctrine storage backend:
composer require doctrine/orm doctrine/dbal
The JSONL backend does not register or resolve Doctrine services. Existing page-views.jsonl files remain readable while new events are written to monthly page-views-YYYY-MM.jsonl partitions.
Doctrine entity mappings are registered automatically. Your application still owns schema migrations; generate and review a migration after enabling Doctrine storage.
Configuration
Register the bundle in config/bundles.php:
return [ // ... Bahdan\PrivacyAnalyticsBundle\PrivacyAnalyticsBundle::class => ['all' => true], ];
Configure the storage and privacy secret:
# config/packages/privacy_analytics.yaml privacy_analytics: secret: '%kernel.secret%' storage: doctrine retention_days: 90 summary_cache_ttl: 60 custom_bot_patterns: - your-internal-monitor
Browser beacon
For client-side page views, configure privacy_analytics.mode: beacon and install
the bundle assets during deployment:
php bin/console assets:install public
Replace any existing inline analytics beacon with the bundle script in your base template:
<script src="/bundles/privacyanalytics/beacon.js" defer></script>
Alternatively, serve the script through Symfony by importing its controller:
privacy_analytics_script: resource: '@PrivacyAnalyticsBundle/src/Presentation/Http/BeaconScriptController.php' type: attribute
For bundles resolved at the package root, the resource above is relative to that
root. Include /api/pa/beacon.js in the script tag when using this route. This
also supports deployments with a separate static-file image.
Set data-beacon-path="/analytics/hit" if your beacon route uses a custom path.
The route must match the configured privacy_analytics.beacon_path.
The script skips browsers reporting navigator.webdriver, honors Do Not Track
and Global Privacy Control, and records each page once after it becomes visible.
Prerendered and background pages wait until activation or visibility. Automated
browsers can conceal WebDriver, so recorded page views do not establish human
identity. No interaction requirement or persistent visitor identifier is added.
High-traffic ingestion with Messenger
Install Messenger, enable async ingestion, and route the bundle message to your asynchronous transport:
composer require symfony/messenger
# config/packages/privacy_analytics.yaml privacy_analytics: async: true # config/packages/messenger.yaml framework: messenger: transports: analytics: '%env(MESSENGER_TRANSPORT_DSN)%' routing: Bahdan\PrivacyAnalyticsBundle\Message\RecordPageView: analytics
Run a normal Messenger worker for the analytics transport. With Doctrine storage, dashboard summaries remain database-side aggregate queries. JSONL summaries stream only partitions that can overlap the requested period; Doctrine is recommended for sustained high traffic.
Dashboard data
Inject Bahdan\PrivacyAnalyticsBundle\Application\TrafficAnalytics into a controller and return summary() as JSON or render it in Twig. The stable result contains seven-day and thirty-day totals, sources, referring domains, top paths, and a 30-day daily series, so applications can use Chart.js or their existing design system without the bundle imposing frontend dependencies.
#[Route('/admin/analytics', methods: ['GET'])] public function analytics(TrafficAnalytics $analytics): JsonResponse { return $this->json($analytics->summary(new \DateTimeImmutable('now', new \DateTimeZone('UTC')))); }
License
MIT