dennisvanbeersel/symfony-logger-client
🛡️ Privacy-First Error Tracking for Symfony - Hosted in EU
Resilience-first error tracking with integrated JavaScript SDK - your app never slows down ⚡
Quick Start • Why This Bundle? • Features • Documentation
# 1. Install
composer require dennisvanbeersel/symfony-logger-client
# 2. Configure (config/packages/application_logger.yaml)
application_logger:
dsn: '%env(APPLICATION_LOGGER_DSN)%'
api_key: '%env(APPLICATION_LOGGER_API_KEY)%'
# 3. Add credentials to .env
APPLICATION_LOGGER_DSN=https://applogger.eu/your-project-uuid
APPLICATION_LOGGER_API_KEY=your-64-character-api-key-here
# 4. Clear cache
php bin/console cache:clear
Done! All PHP exceptions and JavaScript errors are now automatically tracked. No code changes needed.
AppLogger (applogger.eu) is an EU-hosted, privacy-first error tracking SaaS platform specifically designed for Symfony applications. This bundle provides zero-config integration with production-grade resilience.
Most error tracking solutions have a critical flaw: they can slow down or even crash your application when the tracking service is down. This bundle is different.
We achieve this through battle-tested resilience patterns:
| Feature | This Bundle | Typical Solutions | Impact |
|---|---|---|---|
| Timeout | ⚡ 2s max (configurable) | ⏰ Often 30s+ or none | 50ms vs 30s+ delay |
| Circuit Breaker | ✅ Automatic failover | ❌ Keep retrying | Stops wasting resources |
| Fire & Forget | ✅ Returns instantly | ❌ Waits for response | <1ms vs 2000ms |
| Exception Safety | ✅ Never throws | ⚠️ Can crash app | 100% uptime guarantee |
| JS Offline Queue | ✅ localStorage backup | ❌ Errors lost | Zero data loss |
| JS Rate Limiting | ✅ Token bucket | ❌ Can overwhelm API | Protected from error storms |
Without resilience patterns:
// API is down, timeout is 30s
$start = microtime(true);
errorTracker()->captureException($e); // Blocks for 30 seconds!
$elapsed = microtime(true) - $start; // 30,000ms
// User waited 30 seconds for page to load 😱
With this bundle:
// API is down, circuit breaker is open
$start = microtime(true);
errorTracker()->captureException($e); // Returns instantly
$elapsed = microtime(true) - $start; // <1ms
// User doesn't notice anything 🎉
Automatic Capture
Resilience (Production-Grade)
Security (GDPR Compliant)
Developer Experience
No separate npm package needed! The JavaScript SDK is bundled with this Symfony bundle.
Automatic Capture
Resilience (Client-Side)
This bundle provides comprehensive monitoring for both backend and frontend:
composer require dennisvanbeersel/symfony-logger-client
If you're not using Symfony Flex, register the bundle in config/bundles.php:
return [
// ...
ApplicationLogger\Bundle\ApplicationLoggerBundle::class => ['all' => true],
];
# config/packages/application_logger.yaml
application_logger:
dsn: '%env(APPLICATION_LOGGER_DSN)%'
Add to .env:
APPLICATION_LOGGER_DSN=https://public_key@logger.example.com/project_id
APP_VERSION=1.0.0 # Optional but recommended
# config/packages/application_logger.yaml
application_logger:
# Required: Your AppLogger DSN (get from applogger.eu dashboard)
dsn: '%env(APPLICATION_LOGGER_DSN)%'
# Optional: Enable/disable the bundle
enabled: true
# Optional: Application version for release tracking
release: '%env(APP_VERSION)%'
# Optional: Environment identifier
environment: '%kernel.environment%'
# Resilience Settings
timeout: 2.0 # API timeout (0.5-5.0 seconds)
retry_attempts: 0 # Retry failed requests (0-3, 0=fail fast)
async: true # Fire-and-forget mode (recommended)
# Circuit Breaker
circuit_breaker:
enabled: true # Enable circuit breaker pattern
failure_threshold: 5 # Open after N consecutive failures
timeout: 60 # Stay open for N seconds
half_open_attempts: 1 # Test requests before closing
# What to Capture
capture_level: error # Monolog level: debug, info, warning, error, critical
# Breadcrumbs
max_breadcrumbs: 50 # Maximum breadcrumbs to keep (10-100)
# Security: Sensitive Data Scrubbing
scrub_fields:
- password
- token
- api_key
- secret
- authorization
- credit_card
- ssn
# Session Tracking (Required for session replay)
session_tracking:
enabled: true # Enable automatic session tracking (default: true)
track_page_views: true # Track page views as session events (default: true)
idle_timeout: 1800 # Session idle timeout in seconds (default: 30 min)
# Error-Triggered Session Replay
session_replay:
enabled: true # Enable session replay (default: true)
buffer_before_error_seconds: 30 # Seconds to buffer before error (5-60, default: 30)
buffer_before_error_clicks: 10 # Clicks to buffer before error (1-15, default: 10)
buffer_after_error_seconds: 30 # Seconds to buffer after error (5-60, default: 30)
buffer_after_error_clicks: 10 # Clicks to buffer after error (1-15, default: 10)
click_debounce_ms: 1000 # Click debounce delay (100-5000ms, default: 1000)
snapshot_throttle_ms: 1000 # DOM snapshot throttle (500-5000ms, default: 1000)
max_snapshot_size: 1048576 # Max snapshot size in bytes (default: 1MB)
session_timeout_minutes: 30 # Cross-page session timeout (5-120 min, default: 30)
max_buffer_size_mb: 5 # Max localStorage size (1-20MB, default: 5MB)
expose_api: true # Expose JS API for user control (default: true)
# JavaScript SDK
javascript:
enabled: true # Enable Twig globals for JS SDK
auto_inject: true # Auto-inject init script (recommended)
debug: false # Enable console.log debugging
# Debug
debug: '%kernel.debug%' # Enable internal logging
php bin/console cache:clear
Done! All exceptions are now automatically tracked. Visit your AppLogger dashboard at applogger.eu to see errors.
The bundle automatically captures:
No code changes required! Just install and configure.
Send error-level logs to AppLogger:
# config/packages/monolog.yaml
monolog:
handlers:
application_logger:
type: service
id: ApplicationLogger\Bundle\Monolog\Handler\ApplicationLoggerHandler
level: error
channels: ['!event'] # Exclude to avoid duplication
Now all $logger->error(), $logger->critical(), etc. calls are tracked.
For custom error handling:
use ApplicationLogger\Bundle\Service\ApiClient;
use ApplicationLogger\Bundle\Service\BreadcrumbCollector;
class PaymentService
{
public function __construct(
private ApiClient $apiClient,
private BreadcrumbCollector $breadcrumbs
) {}
public function processPayment(Order $order): void
{
// Add breadcrumb for context
$this->breadcrumbs->add([
'type' => 'user',
'category' => 'payment',
'message' => 'Processing payment',
'data' => ['order_id' => $order->getId()],
]);
try {
$this->chargeCustomer($order);
} catch (\Exception $e) {
// Manual error reporting
$this->apiClient->sendError([
'exception' => [
'type' => $e::class,
'value' => $e->getMessage(),
'stacktrace' => $this->formatStackTrace($e),
],
'level' => 'error',
'tags' => ['feature' => 'payment'],
]);
throw $e; // Re-throw if needed
}
}
}
Track user actions leading up to errors:
use ApplicationLogger\Bundle\Service\BreadcrumbCollector;
class CheckoutController extends AbstractController
{
public function __construct(
private BreadcrumbCollector $breadcrumbs
) {}
#[Route('/checkout/step-1')]
public function step1(): Response
{
$this->breadcrumbs->add([
'type' => 'navigation',
'category' => 'checkout',
'message' => 'User entered checkout',
'level' => 'info',
]);
// ... your code
}
}
Default behavior - no setup needed!
The bundle automatically:
window.appLogger availableJust install the bundle - JavaScript tracking works immediately!
If you want control over when/where the SDK loads:
# config/packages/application_logger.yaml
application_logger:
javascript:
auto_inject: false # Disable automatic injection
Then manually add to your templates:
{# templates/base.html.twig #}
<!DOCTYPE html>
<html>
<body>
{% block body %}{% endblock %}
{# Manually place the initialization script #}
{{ application_logger_init() }}
</body>
</html>
Once loaded, use window.appLogger:
// Capture exceptions
try {
riskyOperation();
} catch (error) {
window.appLogger.captureException(error, {
tags: { component: 'checkout' },
extra: { orderId: 12345 }
});
}
// Capture messages
window.appLogger.captureMessage('Payment processed', 'info');
// Add breadcrumbs
window.appLogger.addBreadcrumb({
type: 'user',
message: 'User clicked checkout button',
data: { cartTotal: 99.99 }
});
// Set user context
window.appLogger.setUser({
id: 'user-123',
email: 'user@example.com'
});
// Check circuit breaker status
window.appLogger.transport.getStats();
// {queueSize: 0, rateLimitTokens: 9.2, circuitBreaker: {state: 'closed'}}
Problem: When the API is down, your app wastes resources retrying.
Solution: Circuit breaker with three states:
CLOSED (normal) → [5 failures] → OPEN (service down)
↓
[60 seconds wait]
↓
CLOSED ← [success] ← HALF_OPEN ← [timeout passed]
[failure] → OPEN
PHP Implementation:
JavaScript Implementation:
Monitoring:
// PHP
$state = $apiClient->getCircuitBreakerState();
// ['state' => 'closed', 'failureCount' => 2, 'openedAt' => null]
// JavaScript
window.appLogger.transport.circuitBreaker.getState();
// {state: 'closed', failureCount: 0, openedAt: null}
PHP:
JavaScript:
AbortController to forcefully abortWhen async: true (default):
// With async: false (synchronous)
$start = microtime(true);
$apiClient->sendError($payload);
$elapsed = microtime(true) - $start;
// $elapsed could be 2000ms (full timeout)
// With async: true (fire-and-forget)
$start = microtime(true);
$apiClient->sendError($payload);
$elapsed = microtime(true) - $start;
// $elapsed is typically < 1ms (request queued, method returns)
When API is unreachable:
Handles quota errors gracefully:
Token bucket algorithm prevents error storms:
window.appLogger.transport.getStats();
// {rateLimitTokens: 8.5, queueSize: 0, ...}
Prevents sending the same error repeatedly:
Problem: When user closes tab, errors in queue are lost.
Solution: navigator.sendBeacon() API
beforeunload and visibilitychangeSensitive data automatically removed from error reports:
Default scrubbed fields:
How it works:
[REDACTED]Example:
$request->request->all();
// ['email' => 'user@example.com', 'password' => 'secret123']
// Sent to API as:
// ['email' => 'user@example.com', 'password' => '[REDACTED]']
Custom scrub fields:
application_logger:
scrub_fields:
- password
- credit_card
- my_custom_secret
IPv4: Masks last octet
192.168.1.100 → 192.168.1.0
IPv6: Masks last 80 bits
2001:0db8:85a3:0000:0000:8a2e:0370:7334
→ 2001:0db8:85a3:0000:0000:0000:0000:0000
Why: GDPR compliance - IP addresses are personal data.
# config/packages/dev/application_logger.yaml
application_logger:
enabled: false
Or use .env.local:
APPLICATION_LOGGER_ENABLED=false
Send errors to different AppLogger projects:
# config/services.yaml
services:
app.logger.project_a:
class: ApplicationLogger\Bundle\Service\ApiClient
arguments:
$dsn: '%env(LOGGER_DSN_PROJECT_A)%'
$timeout: 2.0
$circuitBreaker: '@ApplicationLogger\Bundle\Service\CircuitBreaker'
app.logger.project_b:
class: ApplicationLogger\Bundle\Service\ApiClient
arguments:
$dsn: '%env(LOGGER_DSN_PROJECT_B)%'
$timeout: 2.0
$circuitBreaker: '@ApplicationLogger\Bundle\Service\CircuitBreaker'
use ApplicationLogger\Bundle\Service\ApiClient;
use ApplicationLogger\Bundle\Service\BreadcrumbCollector;
use ApplicationLogger\Bundle\Service\ContextCollector;
class CustomErrorHandler
{
public function __construct(
private ApiClient $apiClient,
private ContextCollector $contextCollector,
private BreadcrumbCollector $breadcrumbs
) {}
public function handleBusinessError(BusinessException $e): void
{
$this->apiClient->sendError([
'exception' => [
'type' => $e::class,
'value' => $e->getMessage(),
'stacktrace' => $this->formatTrace($e),
],
'level' => 'warning', // Business errors are warnings
'context' => $this->contextCollector->collectContext(),
'breadcrumbs' => $this->breadcrumbs->get(),
'tags' => [
'error_type' => 'business',
'rule' => $e->getBusinessRule(),
],
]);
}
}
1. Check bundle is enabled:
php bin/console debug:config application_logger
2. Check DSN is correct:
php bin/console debug:container --parameters | grep application_logger.dsn
3. Check circuit breaker:
$cbState = $this->apiClient->getCircuitBreakerState();
// If state is 'open', wait 60s or clear cache
4. Enable debug mode:
application_logger:
debug: true
Check var/log/dev.log for details.
Solution 1: Wait for timeout (default 60 seconds)
Solution 2: Clear cache:
php bin/console cache:clear
Solution 3: Manually reset:
$cache->delete('app_logger_circuit_breaker_state');
1. Check AssetMapper:
php bin/console debug:asset-map | grep application-logger
2. Check browser console for import errors
3. Verify meta tag exists:
<meta name="app-logger-dsn" content="https://...">
Correct format:
https://public_key@your-host.com/project_id
Common mistakes:
❌ http://public_key@host/project (use https://)
❌ https://host/project (missing public_key@)
❌ https://public_key:secret@host/proj (secret not needed)
❌ https://public_key@host (missing /project_id)
composer lint # PHP-CS-Fixer + PHPStan
composer cs-check # Check PSR-12
composer cs-fix # Auto-fix PSR-12
composer phpstan # Static analysis (level 6)
npm run lint # ESLint
npm run lint:fix # Auto-fix ESLint
# PHP tests
composer test
vendor/bin/phpunit
# JavaScript tests
npm test
npm run test:coverage
Minimum:
Recommended:
| Document | Description |
|---|---|
| AppLogger Website | Sign up and get your DSN |
| API Reference | REST API documentation |
| Architecture | Technical architecture |
| Security & Testing | Security practices and testing guidelines |
Part of the AppLogger project - see main LICENSE file.
Key Design Principles:
Built with ❤️ for the Symfony community.
Questions? Issues? Feedback?
How can I help you explore Laravel packages today?