@hexdn/analytics
v0.3.0
Published
Independent, cumulative browser playback analytics for HexDN
Readme
@hexdn/analytics
Independent playback measurement for an existing HTML media element. No HLS, React, or HexDN storage is required. Private server credentials stay out of the browser.
For HexDN collection, first provision a collection key on your trusted server
using @hexdn/sdk and your private environment API key:
import { HexDN } from "@hexdn/sdk";
const hexdn = new HexDN({ apiKey: configuration.hexdnApiKey });
const collectionKey = await hexdn.analytics.collectionKeys.create({
origins: ["https://app.example"],
});Use your application's exact origins (scheme, host and port), without wildcards.
Provision once per origin configuration, not per view. Publish the returned
collectionKey.id in your browser configuration; keep the environment API key
private. Replace the key below with that returned ID. The SDK handles the
collector URL and reporting. Collection-key origins are independent of playback
and upload origin settings.
import { monitorPlayback } from "@hexdn/analytics";
const analytics = monitorPlayback(videoElement, {
video: { id: "your-stable-video-id" },
collectionKey: "ack_browser_publishable_key",
});Production collection needs only the key. To carry the server client's
environment choice into the browser, use
hexdn.analytics.collectionConfig(collectionKey.id). It returns browser-safe
configuration without making a request. Pass it directly:
const analytics = monitorPlayback(videoElement, {
...collectionConfig,
video: { id: "your-stable-video-id" },
});If play requires asynchronous source acquisition, record intent at the start of the play handler to include startup failures:
analytics.intent();
// Resolve the source, then play. Report exhausted failures with recordError().When the element is mounted only after acquisition, start the same measurement
earlier with beginPlaybackAttempt({ video, collectionKey }) (and the same
optional collection configuration). Pass its handle as pendingAttempt to
monitorPlayback() (or the React player's analytics prop). This transfers the
original attempt and reporter once, including startup time. The monitor inherits
pendingAttempt.video, so the video identity does not need to be supplied
again. Before attachment, call
pendingAttempt.recordError(code, { fatal: true }) if acquisition fails, or
destroy() if the viewer leaves. After attachment the media monitor owns
cleanup; destroying the pending handle is harmless.
If the tenant explicitly refuses access, call recordAccessDenied() on the
pending handle or monitor. This ends the attempt as denied without increasing
technical errors. Do not infer a tenant denial from a delivery, proof, or
expired-credential HTTP 401/403; those remain under the playback recovery
policy.
On a real content change, before replacing the source:
analytics.setVideo({ id: "next-video-id" });On teardown, remove listeners and make a final best-effort keepalive report:
analytics.destroy();When analytics is disabled or consent is withdrawn, use
destroy({ flush: false }) instead, including on a pending pre-media handle.
This discards buffered reports and scheduled retries without a final send.
Already-issued requests cannot be recalled. The React player applies this when
its analytics prop is removed; ordinary unmount still flushes.
Attach once, before play. Duplicate attachment throws; share subscribe()
observations with resume persistence or product rules. Subscriber failures
cannot interrupt playback. Use a custom asynchronous
transport(report, { keepalive }) to route reports through a first-party
backend; resolve only after durable acceptance, and reject on failure. Choose
either a collectionKey or a custom transport. The lower-level
createFetchTransport({ collectionKey }) helper is also available, but ordinary
collection does not need to construct a transport. The default fetch transport
sends a bounded { key, report } JSON envelope as text/plain, without cookies
or private API credentials, and times out requests after ten seconds.
Measurement and lifecycle
- Play intent creates an attempt without waiting for the network.
playingestablishes successful playback. First-frame startup time usesrequestVideoFrameCallbackwhere available, orrecordFirstFrame()supplied by an integration. Missing first-frame timing, including attaching after playback has already started, remainsnull. - Playing time uses monotonic elapsed time validated by media advancement and playback rate. Pauses, seeks and buffering do not count as playing. Replaying a portion increases playing time; coverage remains its union.
- Coverage is approximate: 100 media-time bucket midpoints crossed during valid
playback. Duration is fixed at the first known finite duration, up to seven
days. Unknown or infinite duration leaves duration and coverage
null. - Playing time is split across UTC days using the attempt's initial wall-clock anchor and monotonic elapsed time. Wall-clock changes within a view do not move previously reported activity to another day.
- A pause/resume, credential renewal, source URL replacement, or recovered error
preserves the attempt.
setVideo()changes identity only when the supplied video identifiers change. An ended view followed by play creates a new attempt; seeking backward before ending preserves the current attempt. - Attempts expire after 30 minutes without playing activity, or 24 hours total. Further playback begins a fresh attempt. Content changes and teardown mark an active attempt abandoned; page hiding alone does not establish abandonment.
- Native media errors are fatal by default. Engines that recover them must set
nativeErrorsFatal: false, then callrecordError(code, { fatal: true })when recovery is exhausted. Error codes are bounded tokens, never signed URLs. - Explicit tenant access denial is a separate terminal outcome. Earlier playing time and recovered errors remain recorded when access is withdrawn mid-view.
Reporting and retention boundaries
Reports contain cumulative summaries, not a raw event log. Each attempt has a stable UUID and increasing revisions; retries reuse the exact immutable payload. Normal reports coalesce to roughly 30 seconds while measurements change, with immediate start, pause, seek, end and error flushes. The interval can be configured between 5 and 60 seconds. Visibility/page lifecycle flushes use keepalive and may overtake an ordinary request; the server must deduplicate and order revisions.
Client memory holds at most four latest pending attempts. An unchanged failed snapshot receives at most five automatic retries with bounded backoff; new measurements can resume sending. A long outage can evict the oldest pending attempt. There is no cross-reload browser persistence, and unload delivery is best-effort. Missing final reports remain unknown outcomes. Playback never waits for collection and reports are not proof of human attention.
The browser-independent @hexdn/analytics/protocol export provides the report
types, limits, strict validator, and
isAnalyticsReportSuccessor(previous, next). The server derives environment
scope from collection authorization, rejects stale or conflicting revisions,
checks its own acceptance/clock window, and commits accepted deltas atomically.
Terminal snapshots are immutable. The validator does not provide authentication,
retention, rate limiting, or a proof of watching.
No viewer identity, resume position persistence, raw URLs, arbitrary dimensions, or tenant-specific public-view rules are stored by this package.
