*/ public readonly array $data; public readonly float $at; /** * @param array $data */ public function __construct(public readonly string $name, array $data = [], ?float $at = null) { if (!in_array($name, self::NAMES, true)) { throw new \InvalidArgumentException('Unknown event name.'); } $this->data = self::sanitize($data); $this->at = $at ?? microtime(true); } /** * Keep logged values small and metadata-shaped as well as key-allow-listed. Event data may originate in * extensions or provider responses, so arbitrary nested values and free-form strings must never reach storage. * * @param array $data * @return array */ private static function sanitize(array $data): array { $safe = []; foreach ($data as $key => $value) { if (!is_string($key) || !in_array($key, self::KEYS, true)) { continue; } if (in_array($key, self::INTEGER_KEYS, true)) { if (is_int($value) || (is_string($value) && preg_match('/^-?\\d+$/D', $value) === 1)) { $safe[$key] = max(0, min(PHP_INT_MAX, (int) $value)); } continue; } if ($key === 'tainted') { if (is_bool($value)) { $safe[$key] = $value; } continue; } if (in_array($key, self::LIST_KEYS, true)) { if (!is_array($value)) { continue; } $items = []; foreach (array_slice($value, 0, self::MAX_LIST_ITEMS) as $item) { $normalized = self::safeCode($item); if ($normalized !== null) { $items[] = $normalized; } } $safe[$key] = $items; continue; } $normalized = self::safeCode($value); if ($normalized !== null) { $safe[$key] = $normalized; } } return $safe; } private static function safeCode(mixed $value): ?string { if (!is_string($value) || $value === '' || strlen($value) > self::MAX_STRING_BYTES) { return null; } return preg_match('/^[A-Za-z0-9][A-Za-z0-9_.:\\/+-]*$/D', $value) === 1 ? $value : null; } /** * @since 0.1.0 * @return array{v: int, name: string, at: float, data: array} */ public function toArray(): array { return ['v' => self::VERSION, 'name' => $this->name, 'at' => $this->at, 'data' => $this->data]; } }