Analytics
in package
Records security events into per-day and per-hour aggregate counters and, when the detailed event log is enabled, into a dedicated database table.
See the module-6 and the analytics-dashboard design specs.
Table of Contents
Constants
- COUNTER_RETENTION_DAYS = 90
- Aggregate daily-counter retention, in days.
- HOURLY_RETENTION_HOURS = 48
- Aggregate hourly-counter retention, in hours.
- OPTION = 'creationell_captcha_analytics'
- The option holding the aggregate daily counters.
- OPTION_HOURLY = 'creationell_captcha_analytics_hourly'
- The option holding the aggregate hourly counters (24-hour view).
- TYPES = ['verified', 'failed', 'firewall', 'ratelimit', 'underattack', 'underattack_passed', 'challenge']
- The seven recognised event types.
Properties
- $daily_deltas : array<string, array<string, int>>
- Pending daily counter deltas: { bucket => { type => count } }. Flushed on `shutdown` so a request that triggers N events incurs at most one get_option/update_option round-trip per counter table, regardless of N.
- $flush_hooked : bool
- Whether the shutdown flush has already been hooked for this request.
- $hourly_deltas : array<string, array<string, int>>
- Pending hourly counter deltas: { bucket => { type => count } }.
Methods
- clear_events() : int
- Deletes every row from the event-log table.
- count_events() : int
- Counts the event-log rows matching the given filter.
- ensure_table() : void
- Creates or updates the event-log table. Idempotent — safe to call repeatedly.
- flush_pending_deltas() : void
- Flushes the accumulated daily and hourly counter deltas back to the options table. Reads the current value first and adds the deltas, so concurrent updates from parallel requests are not lost (the underlying get+update is still non-atomic, but only one round-trip per request narrows the race window considerably compared to one round-trip per event).
- get_daily_counts() : array<string, array<string, int>>
- Returns the aggregate daily counters, keyed by date.
- get_hourly_counts() : array<string, array<string, int>>
- Returns the aggregate hourly counters, keyed by 'Y-m-d H' (UTC).
- prune_events() : int
- Deletes event-log rows older than the configured retention period.
- query_events() : array<int, array<string, string>>
- Returns event-log rows matching the given filter, newest first.
- record() : void
- Records one security event: bumps the daily and hourly counters and — when the detailed event log is enabled and the per-type toggle is on — writes a table row. Never fatals.
- table_exists() : bool
- Whether the event-log table currently exists.
- apply_deltas() : void
- Merges the given { bucket => { type => count } } deltas into the option value and prunes buckets older than the retention window.
-
build_event_filter()
: array{0: string, 1: array
} - Builds the WHERE clause and bound parameters for an event-log filter.
- bump_counter() : void
- Accumulates today's counter delta in the request-local cache.
- bump_hourly_counter() : void
- Accumulates the current hour's counter delta in the request-local cache.
- current_path() : string
- The current request path for the event log.
- ensure_event_type_index() : void
- Idempotently adds the composite (event_type, created_at) index to the events table. Dashboard queries that filter by event_type benefit from a covering composite, whereas the standalone created_at index alone forces a filesort over a typed slice.
- ensure_flush_hook() : void
- Idempotently registers the shutdown flush. Called the first time a delta is recorded in this request.
- log_row() : void
- Writes one row into the event-log table and occasionally prunes it.
- server_value() : string
- Reads a $_SERVER header value, sanitised and length-capped.
- table_name() : string
- The fully-qualified event-log table name.
- verification_data() : string
- The submitted ALTCHA solution payload for the current request, capped.
Constants
COUNTER_RETENTION_DAYS
Aggregate daily-counter retention, in days.
private
mixed
COUNTER_RETENTION_DAYS
= 90
HOURLY_RETENTION_HOURS
Aggregate hourly-counter retention, in hours.
private
mixed
HOURLY_RETENTION_HOURS
= 48
OPTION
The option holding the aggregate daily counters.
private
mixed
OPTION
= 'creationell_captcha_analytics'
OPTION_HOURLY
The option holding the aggregate hourly counters (24-hour view).
private
mixed
OPTION_HOURLY
= 'creationell_captcha_analytics_hourly'
TYPES
The seven recognised event types.
private
mixed
TYPES
= ['verified', 'failed', 'firewall', 'ratelimit', 'underattack', 'underattack_passed', 'challenge']
Properties
$daily_deltas
Pending daily counter deltas: { bucket => { type => count } }. Flushed on `shutdown` so a request that triggers N events incurs at most one get_option/update_option round-trip per counter table, regardless of N.
private
static array<string, array<string, int>>
$daily_deltas
= []
$flush_hooked
Whether the shutdown flush has already been hooked for this request.
private
static bool
$flush_hooked
= false
$hourly_deltas
Pending hourly counter deltas: { bucket => { type => count } }.
private
static array<string, array<string, int>>
$hourly_deltas
= []
Methods
clear_events()
Deletes every row from the event-log table.
public
clear_events() : int
Return values
int —Number of rows deleted.
count_events()
Counts the event-log rows matching the given filter.
public
count_events(array<string, mixed> $args) : int
Parameters
- $args : array<string, mixed>
-
Filter args (id, search, event_type, date_from, date_to).
Return values
intensure_table()
Creates or updates the event-log table. Idempotent — safe to call repeatedly.
public
ensure_table() : void
flush_pending_deltas()
Flushes the accumulated daily and hourly counter deltas back to the options table. Reads the current value first and adds the deltas, so concurrent updates from parallel requests are not lost (the underlying get+update is still non-atomic, but only one round-trip per request narrows the race window considerably compared to one round-trip per event).
public
static flush_pending_deltas() : void
get_daily_counts()
Returns the aggregate daily counters, keyed by date.
public
get_daily_counts() : array<string, array<string, int>>
Return values
array<string, array<string, int>>get_hourly_counts()
Returns the aggregate hourly counters, keyed by 'Y-m-d H' (UTC).
public
get_hourly_counts() : array<string, array<string, int>>
Return values
array<string, array<string, int>>prune_events()
Deletes event-log rows older than the configured retention period.
public
prune_events() : int
Return values
int —Number of rows deleted.
query_events()
Returns event-log rows matching the given filter, newest first.
public
query_events(array<string, mixed> $args) : array<int, array<string, string>>
Parameters
- $args : array<string, mixed>
-
Filter args (id, search, event_type, date_from, date_to) plus
limit(1–1000) andoffset(>= 0).
Return values
array<int, array<string, string>>record()
Records one security event: bumps the daily and hourly counters and — when the detailed event log is enabled and the per-type toggle is on — writes a table row. Never fatals.
public
record(string $type[, array<string, mixed> $context = [] ]) : void
Parameters
- $type : string
-
One of the recognised event types.
- $context : array<string, mixed> = []
-
Optional caller-supplied context.
table_exists()
Whether the event-log table currently exists.
public
table_exists() : bool
Return values
boolapply_deltas()
Merges the given { bucket => { type => count } } deltas into the option value and prunes buckets older than the retention window.
private
static apply_deltas(string $option, array<string, array<string, int>> $deltas, int $retention, string $bucket_format, int $bucket_seconds) : void
Parameters
- $option : string
-
Option name to read+write.
- $deltas : array<string, array<string, int>>
-
Pending increments by bucket and type.
- $retention : int
-
Retention amount (days or hours).
- $bucket_format : string
-
gmdate() format for bucket keys.
- $bucket_seconds : int
-
Seconds per retention unit (86400 or 3600).
build_event_filter()
Builds the WHERE clause and bound parameters for an event-log filter.
private
build_event_filter(array<string, mixed> $args) : array{0: string, 1: array}
Parameters
- $args : array<string, mixed>
-
Filter args: id, search, event_type, date_from, date_to.
Return values
array{0: string, 1: arraySQL fragment ('' or ' WHERE …') and params.
bump_counter()
Accumulates today's counter delta in the request-local cache.
private
bump_counter(string $type) : void
Parameters
- $type : string
bump_hourly_counter()
Accumulates the current hour's counter delta in the request-local cache.
private
bump_hourly_counter(string $type) : void
Parameters
- $type : string
current_path()
The current request path for the event log.
private
current_path() : string
Returns only the URI path component — the query string is dropped so tokens passed as GET parameters (magic-login links, API keys, …) do not leak into the persistent event log. Strips control characters (DB hygiene) and caps at 255 bytes. The dashboard escapes the value on output.
Return values
stringensure_event_type_index()
Idempotently adds the composite (event_type, created_at) index to the events table. Dashboard queries that filter by event_type benefit from a covering composite, whereas the standalone created_at index alone forces a filesort over a typed slice.
private
ensure_event_type_index() : void
ensure_flush_hook()
Idempotently registers the shutdown flush. Called the first time a delta is recorded in this request.
private
ensure_flush_hook() : void
log_row()
Writes one row into the event-log table and occasionally prunes it.
private
log_row(string $type[, array<string, mixed> $context = [] ]) : void
Honours the optional IP-anonymisation and request-body-fingerprint toggles from Modul 11c.
Parameters
- $type : string
-
The event type.
- $context : array<string, mixed> = []
-
Caller-supplied context.
server_value()
Reads a $_SERVER header value, sanitised and length-capped.
private
server_value(string $key, int $max) : string
Parameters
- $key : string
-
The $_SERVER key.
- $max : int
-
Maximum length.
Return values
stringtable_name()
The fully-qualified event-log table name.
private
table_name() : string
Return values
stringverification_data()
The submitted ALTCHA solution payload for the current request, capped.
private
verification_data() : string