@morescreens/log-collector
v0.7.1
Published
Structured logging library for client applications.
Readme
@morescreens/log-collector
Structured logging library for client applications.
Features
- Multiple log types — standard logs, watching activity telemetry, ad activity telemetry, content events (zap/playback), and push notification logging
- Log levels — Debug, Info, Warn, Error, Critical with configurable minimum level
- Two-phase init — construct with minimal config, call
configure()later when remaining fields are available; logs sent before config is complete are deferred and flushed in order once it is, while Error/Critical are sent immediately as soon ascollectorUrlis known - Singleton — create once, retrieve anywhere with
Logger.getInstance() - Session tracking — auto-generated session ID per Logger instance, included in standard log payloads for grouping logs from the same session
- Watch sessions —
startWatchSession()mints a per-viewingwatch_sessionid, stamped onto subsequent watching activity logs so a single viewing's events can be grouped - Request labeling — each request appends
?log=<label>&log_type=<group>query params so logs are distinguishable and groupable in the browser's network inspector - Auto-retry with priority buffer — failed sends are buffered and retried with configurable buffer size; every payload carries a
log_bufferedflag (0/1), and retried entries also carry alog_buffered_datetimetimestamp - Library version tracking — every payload includes
log_source_version(e.g.@morescreens/[email protected]) for downstream filtering - TypeScript-first — full type definitions, ESM only
Installation
npm install @morescreens/log-collectorQuick Start
import {
Logger,
LogLevel,
LogValidationError,
} from "@morescreens/log-collector"
import type {
LoggerOptions,
LogEntry,
WatchActivityEntry,
AdActivityEntry,
ContentEventEntry,
} from "@morescreens/log-collector"
// 1. Create the singleton (only logSource is required)
const logger = new Logger({ logSource: "my-app" })
// 2. Configure remaining fields when available
logger.configure({
collectorUrl: "https://collector.example.com/log",
installation: "abc-123",
uuid: "device-uuid",
applicationInstallationId: 42,
applicationPublicationId: "pub-1",
cappVersion: "1.0.0",
timezone: "Europe/Sarajevo",
logLevel: LogLevel.Warn,
})
// 3. Send logs
logger.warn({ message: "Retry attempt failed" })
logger.error({ message: "Something failed", tag: "network" })
// 4. Retrieve the singleton from anywhere
const same = Logger.getInstance()Log Types
Standard Logs
General-purpose application logging with five severity levels. Messages below the configured logLevel are discarded.
| Method | Level | Enum Value |
| ---------- | -------- | ---------- |
| debug | Debug | 2 |
| info | Info | 3 |
| warn | Warn | 4 |
| error | Error | 5 |
| critical | Critical | 6 |
logger.debug({ message: "EPG data loaded" })
logger.info({ message: "Channel changed", tag: "ZappLog" })
logger.warn({
message: "Retry attempt failed",
valueStr1: url,
valueInt1: retryCount,
})
logger.error({
message: "API request failed",
valueStr1: "GET /api/channels",
valueStr2: "503",
})
logger.critical({ message: `${err.message} stack: ${err.stack}` })Each method accepts a LogEntry:
| Field | Type | Required |
| ------------- | -------- | -------- |
| message | string | Yes |
| tag | string | No |
| type | string | No |
| targetIndex | string | No |
Additional optional fields:
| Field | Type |
| ------------------------- | -------- |
| valueInt1 – valueInt4 | number |
| valueStr1 – valueStr4 | string |
Raw logs (bypass the level filter)
raw(level, entry) sends a standard log at the given level without applying the logLevel filter — for logs that must always be delivered regardless of the configured minimum level. It accepts the same LogEntry as the level methods; the only difference is that the filter is skipped.
logger.raw(LogLevel.Debug, { message: "Always-on diagnostic", tag: "Heartbeat" })Watching Activity
Content viewing telemetry sent via sendWatchingActivity(). Used to track what users are watching, with metadata about the content and series. Automatically sets targetIndex to "watching_activity_log".
logger.sendWatchingActivity({
message: "PLAY",
contentId: 12345,
contentKind: "vod",
intervalDuration: 30,
})| Field | Type | Required |
| ---------------------- | --------- | -------- |
| message | string | Yes |
| contentId | number | Yes |
| contentExternalId | string | No |
| contentAssetType | string | No |
| contentKind | string | No |
| contentAudioOnly | boolean | No |
| contentOriginalTitle | string | No |
| contentProviderName | string | No |
| contentSeriesId | string | No |
| deliveryMethod | string | No |
| seriesTitle | string | No |
| seriesOriginalTitle | string | No |
| seriesSeason | number | No |
| seriesEpisode | number | No |
| epgId | number | No |
| epgTitle | string | No |
| intervalDuration | number | No |
| catchupTime | number | No |
| partnerName | string | No |
| valueInt | number | No |
| valueStr | string | No |
Watch sessions. Call startWatchSession() at the start of each viewing (e.g. on play/restart) to mint a fresh watch_session id. It is stamped onto every subsequent sendWatchingActivity() payload and persists until the next call, so all events of one viewing share an id. Until the first call, watch_session is omitted. It applies to watching activity only — ad activity logs do not carry it.
Ad Activity
Ad telemetry sent via sendAdActivityLog(). Extends watching activity with ad-specific fields. Automatically sets targetIndex to "ad_activity_log".
logger.sendAdActivityLog({
message: "AD_PLAY",
contentId: 12345,
adCreativeId: "creative-1",
adFullscreen: true,
adMuted: false,
adWatched: 50,
adSkipped: false,
adTitle: "Summer Sale 2026",
})Accepts all WatchActivityEntry fields plus the following ad fields:
| Field | Type | Required | Description |
| -------------- | --------- | -------- | ------------------------------------ |
| adCreativeId | string | Yes | Ad creative identifier |
| adFullscreen | boolean | No | Fullscreen flag |
| adMuted | boolean | No | Muted flag |
| adWatched | number | No | Watched quartile (e.g. 0/25/50/75/100) |
| adSkipped | boolean | No | Skipped flag |
| adTitle | string | No | Ad title |
Content Events
A standard log enriched with content metadata, sent via sendContentEvent() — for events tied to a specific content item, such as channel zap and playback. The event kind is identified by tag (not targetIndex); the calling app sets the tag (e.g. "ZappLog", "PlaybackEvent"). Always sent at Info level and not subject to the logLevel filter.
logger.sendContentEvent({
message: "Channel zap",
tag: "ZappLog",
contentId: 12345,
contentTitle: "News at 9",
contentKind: "live",
valueInt1: 350, // e.g. zap time in ms
})Accepts all LogEntry fields (message, tag, type, valueInt1–valueInt4, valueStr1–valueStr4) plus the following content fields:
| Field | Type | Required |
| ---------------------- | --------- | -------- |
| contentId | number | Yes |
| contentTitle | string | No |
| contentKind | string | No |
| contentAssetType | string | No |
| contentAudioOnly | boolean | No |
| contentOriginalTitle | string | No |
| deliveryMethod | string | No |
| partnerName | string | No |
| url | string | No |
| epgId | number | No |
| epgTitle | string | No |
| seriesTitle | string | No |
| seriesOriginalTitle | string | No |
| seriesSeason | number | No |
| seriesEpisode | number | No |
Push Notifications
Logs push notification events via sendPushNotification(). Automatically sets targetIndex to "push_notification_log".
logger.sendPushNotification({ message: "Notification received" })Accepts the same LogEntry shape as standard logs.
API Reference
new Logger(options)
Creates the singleton Logger instance. Only logSource is required at construction; all other fields can be set later via configure(). Calling new Logger() again replaces the singleton.
Logger.getInstance()
Returns the singleton instance. Throws if no Logger has been created yet.
raw(level, entry)
Sends a standard log at the given LogLevel, bypassing the logLevel filter. Accepts the same LogEntry as the level methods.
sendContentEvent(entry)
Sends a content event (e.g. zap or playback) — a standard log enriched with content metadata, identified by entry.tag. Always sent at Info level and not filtered by logLevel.
startWatchSession()
Mints a fresh watch_session id, stamped onto subsequent sendWatchingActivity() payloads until the next call. Call once per viewing (e.g. on play/restart). Watching logs before the first call omit watch_session; ad and standard logs are unaffected.
configure(partial)
Merges additional options into the current configuration. Each field is validated independently: valid fields are applied, while invalid ones are skipped and reported via console.warn. It never throws, so one bad field can't discard the valid fields in the same call. Can be called multiple times — each call merges on top of the previous state, so you can progressively provide fields as they become available.
// Set URL early
logger.configure({ collectorUrl: "https://collector.example.com/log" })
// Add user context later when available
logger.configure({ profileId: 42, subscriberId: 7 })
// A bad field is skipped + warned; the valid one is still applied
logger.configure({ installation: "abc-123", uuid: 123 as any }) // uuid ignored, installation setConfiguration
All fields in LoggerOptions. Fields marked "Required at send" must be set (via constructor or configure()) before calling any send method — otherwise a LogValidationError is thrown.
| Field | Type | Required at init | Required at send | Default |
| --------------------------- | ---------- | ---------------- | ---------------- | ------- |
| logSource | string | Yes | Yes | — |
| collectorUrl | string | No | Yes | — |
| installation | string | No | Yes | — |
| uuid | string | No | Yes | — |
| applicationInstallationId | number | No | Yes | — |
| applicationPublicationId | string | No | Yes | — |
| cappVersion | string | No | Yes | — |
| timezone | string | No | Yes | — |
| logLevel | LogLevel | No | No | Info |
| enableConsoleLog | boolean | No | No | false |
| maxBufferSize | number | No | No | 100 |
| profileId | number | No | No | — |
| subscriberId | number | No | No | — |
Behavior before configuration is complete
Send methods may be called before all "Required at send" fields are set. What happens depends on the log:
- Standard logs below Error (
debug,info,warn, andraw()at those levels) and content events (sendContentEvent) are deferred — held in a bounded in-memory buffer (capped atmaxBufferSize) and flushed in order, each with its original timestamp, as soon asconfigure()supplies the last required field. - Error and Critical (
error,critical, orraw()at those levels) are sent immediately oncecollectorUrlis known, without waiting for the rest of the config — so a failure during startup is still reported. Any required field not yet set is simply omitted from that payload. - If
collectorUrlitself is not set, everything is deferred, including Error/Critical — there is nowhere to send. - Watching and ad activity (
sendWatchingActivity,sendAdActivityLog) are not deferred: they throwLogValidationErrorif required fields are missing.
The first deferral logs a one-time console.warn; deferred logs are flushed with log_buffered: 0 (they were never buffered for retry).
Request labeling
Every request appends ?log=<label>&log_type=<group> query params to collectorUrl, so requests are easy to tell apart in the browser's network inspector (which otherwise shows only the shared path). log is the lowercased event message for watching/ad activity (play, pause, ad_break, …) or the level name for everything else (info, warn, error, …). log_type is the category — watch, ad, or log — so a single filter catches a whole sequence (e.g. log_type=watch). Neither affects the request body and the collector can ignore them.
Buffering & Retry
Failed HTTP sends are automatically buffered and retried on the next send attempt. The buffer holds up to maxBufferSize entries (default: 100) and can be configured via the constructor or configure():
const logger = new Logger({ logSource: "my-app", maxBufferSize: 200 })When the buffer is full, low-priority logs (below Error) are evicted first to preserve error and critical entries. If the buffer is entirely error/critical, the oldest entry is evicted to make room for new high-priority logs.
Every payload carries a log_buffered field: 0 for direct sends and 1 for entries released from the retry buffer. Buffered entries additionally carry log_buffered_datetime, the timestamp of the (re)send attempt that released them (format: "YYYY-MM-DD HH:mm:ss.SSSSSS <timezone>"). On repeated retries the datetime is updated each time, so the value on the delivered payload reflects the final attempt. Direct sends omit log_buffered_datetime entirely — the gap between log_buffered_datetime and log_datetime identifies the retry wait time downstream.
Error Handling
The library throws LogValidationError when the constructor or a send method receives invalid values. It extends Error and includes:
field— the name of the invalid fieldvalue— the offending value
import { LogValidationError } from "@morescreens/log-collector"
try {
logger.info({ message: 123 as any })
} catch (err) {
if (err instanceof LogValidationError) {
console.error(err.field) // "message"
console.error(err.value) // 123
}
}