@pmate/log-sdk
v0.6.0
Published
PMate browser, React and Node logs with access and profile context
Readme
@pmate/log-sdk
Web: initialize once, then report user actions
import { web } from '@pmate/log-sdk/web'
web.init({
key: 'KEY_FROM_DEVELOPERS',
appId: 'pmate:hello',
auto: { pv: true, webVitals: true }, // defaults to both enabled
})
web.track('signup.clicked', { entry: 'home' })
web.identify('profile-alice') // optional self-reported profile selection
web.reset() // clear profile on logout; preserve the anonymous visit
await web.flush()
// await web.close() on application teardown, not on every routeCreate a Web key with pv, behavior and vitals at https://dev.pmate.chat/log-keys.
Existing keys retain their original scopes. If your key excludes a topic, disable
its automatic collection. web.pageView() records a manual visit; turn off
auto.pv when your router explicitly records visits to avoid double counting.
The SDK strips URL query strings/fragments and records available CLS, FCP, INP,
LCP and TTFB separately from PV. SPA changes never move delayed document metrics
to a new route. Metrics remain runtime logs (48h); user events remain event logs (1y).
init is synchronous local configuration; it makes no registration or login request.
The default endpoint is https://log-api.pmate.chat; an optional endpoint supports
another deployment (HTTPS, or HTTP localhost for tests). Same-configuration init
is idempotent, changed configuration requires awaiting close(). Pre-init calls
are ignored with a one-time warning. Importing is SSR-safe; initialize in a client
lifecycle. createWebLogger() provides an isolated instance for tests or multiple
independent applications; normally use the singleton web.
Properties/identity are captured at event time. identify does not sign in a user,
and never takes an auth token. The in-memory queue holds 200 entries by default,
batches at most 50 entries/48 KiB, attempts an entry at most 3 times using the
original ID/time, and flushes every 5 seconds and on hide. flush drains all topics;
close stops listeners/admission and drains. Delivery is best effort; no disk queue.
Use explicit behavior events; do not send personal data in properties or path names.
Native Swift/Kotlin SDKs in parrot-native expose the equivalent
PmateLog.initSDK, screen, track, identify, reset, flush, close API,
with automatic app foreground/background events. Their examples are generated by
the Key console for the selected platform.
Background
PMate logs need to connect anonymous page visits, signed-in Profiles and backend requests without merging different Profiles of one Account. This SDK adds access/profile context to the existing topic log protocol. It reuses @pmate/log for browser delivery and supports the same ingest protocol or a trusted Store sink on Node.
Runtime and Event APIs
Classification follows event meaning, not the runtime or the presence of HTTP. The lower-level browser/React APIs expose separate runtime/event scopes. The Web scenario entry provides direct pageView/track methods.
| API | Meaning | Chain | Retention |
| --- | --- | --- | --- |
| log.runtime.append(topic, entry) / log.runtime.captureError(error) | Program diagnostics, HTTP requests, bot activity, auth, heartbeat, polling, deployment, console, vitals and crashes | app-logs | 48 hours |
| log.event.pageView() | Explicit browser page visit, with current pathname and generated page-view ID | web-logs | 1 year |
| log.event.track(name, properties) | Explicitly defined user behavior | web-logs | 1 year |
React exposes both scopes. Node exposes only runtime; runtime.append rejects pv
and behavior topics. Callers cannot set the category or destination chain.
Retention is enforced by chain configuration, not by the SDK queue.
Automatic page views use the Event route. Browser errors, console and vitals use
Runtime even though they originate in the browser. Intake grants must explicitly
allow each category/topic pair.
const log = useLog() // @pmate/log-sdk/react
log.runtime.append("application", { data: { event: "sync.failed" } })
log.event.track("signup.clicked", { button: "signup" })
log.event.pageView() // use when manually recording a page visitPublic event keys and initialization
Create a log intake key at https://dev.pmate.chat/log-keys. Bind a registered service, application, environment, platform, allowed topics and quota. Creation is the only provisioning step: there is no per-topic init or manual table creation. Instantiate the SDK once per application. The key determines service/environment server-side; no user login or account token is needed to append events. It cannot read logs.
import { createLogger } from '@pmate/log-sdk/browser'
const log = createLogger({
endpoint: 'https://log-api.pmate.chat',
eventKey: 'KEY_FROM_DEVELOPERS', appId: 'pmate:hello',
})
log.event.track('signup.clicked', { source: 'home' })
await log.flush()For native JavaScript clients, use @pmate/log-sdk/event with the same options plus
platform: 'ios' or 'android'. It has no browser/Node dependencies; call
log.event.pageView('/screen') explicitly. Custom clients can POST
{category:'event',topic:'behavior',items:[{id,time,data:{appId,platform,event}}]}
to /event/logs/batch with x-pmate-log-key. IDs and timestamps must be retained
on retries. Profile/access IDs are optional untrusted client claims.
Web keys may restrict exact HTTP(S) Origins. Native keys need no Origin. Quotas are per active intake process (production uses one process); no distributed quota is claimed. Disable/rotation writes are confirmed against the Store index before success; new requests then reject the old key. In-flight writes may complete. Storage errors fail closed. Plaintext keys are returned once; list responses omit digests. The current single management writer serializes mutations; deploying multiple management writers requires storage-level concurrency control first.
An eventKey only sends behavior/PV. Automatic runtime diagnostics require the
existing scoped key plus serviceId/environment; diagnostics are not routed through
the event key. Both keys can be provided to one browser/React logger.
The SDK now exposes only runtime / event. Update previous .app / .web
call sites before upgrading. Existing static grants and old request categories
are explicitly translated at legacy HTTP ingress during migration; physical
app-logs / web-logs chains and historical rows are unchanged.
Identity
accessId is a random tab visit ID. Reload/history restore reuses sessionStorage; a new document navigation starts a visit (including new/duplicated tabs). An SPA route change retains it. After 30 minutes without interaction, the next interaction or explicit event starts another visit. Storage failures fall back to memory. Background vitals do not extend the visit. This is not a persistent visitor ID.
profileId is optional and scoped by appId. Logging in keeps accessId, logout clears profileId, and queued events retain their original identity. No Account-level identity merging occurs. Browser identity is client-reported; Node identity must come from verified server authentication. Neither accessId nor telemetry is an authorization proof.
React
import { LogProvider, useLog } from '@pmate/log-sdk/react'
const options = {
appId: 'pmate:hello', serviceId: 'pmate:hello', environment: 'prod',
endpoint: 'https://log-api.pmate.chat', key: 'REGISTERED_PUBLIC_INTAKE_KEY',
apiOrigins: ['https://api.example.com'],
}
function App({ profileId }) {
return <LogProvider options={options} profileId={profileId}><Content /></LogProvider>
}
function Content() {
const log = useLog()
return <button onClick={() => log.runtime.append('application', {data:{event:'feature.opened'}})}>Open</button>
}Keep provider options stable. Initialization happens after mount, is SSR-safe, and suppresses React StrictMode's discarded initialization. Automatic PV/vitals/errors retain the existing browser SDK behavior. beforeSend can redact application-specific paths or drop an event. Never include credentials or request/response bodies.
Browser
import { createLogger } from '@pmate/log-sdk/browser'
const log = createLogger(options)
log.setProfileId(selectedProfileId) // null on logout
await log.fetch('https://api.example.com/resource')
await log.close()log.fetch adds x-pmate-access-id and x-request-id only to explicitly allowed API origins. It also sends x-pmate-profile-id when a profile is selected; this is an untrusted selection claim, not authentication. The server must verify the selected profile belongs to the authenticated account and expected App before passing it to Node context. It refuses redirects on those requests. APIs must allow these headers in CORS. Global fetch is never patched. Only the wrapped requests carry correlation.
Entry points
React applications report through @pmate/log-sdk/react. Node services import
createLogger and transport helpers from @pmate/log-sdk. The /node export
remains a compatibility alias. @pmate/log is the SDK's internal browser transport;
applications should not initialize a second logger with it.
Node
import { createLogger } from '@pmate/log-sdk'
const log = createLogger({
appId: 'pmate:hello', serviceId: 'pmate:hello-api', environment: 'prod',
endpoint: 'https://log-api.pmate.chat', token: process.env.LOG_INGEST_TOKEN,
})
await log.withRequest(request, {profileId: authenticatedProfile?.id}, async scope => {
log.runtime.append('application', {data:{event:'http.request', requestId:scope.requestId}})
// business work; report completion/error without raw bodies
})
await log.close()Use authenticatedProfile from server authentication, never a client body/header. AsyncLocalStorage isolates concurrent requests and anonymous requests clear inherited identity. For private-network writes, use the SDK sink:
import { createLogger, createAppLogSink } from "@pmate/log-sdk"
const log = createLogger({
appId: "pmate:hello", serviceId: "pmate:hello-api", environment: "prod",
sink: createAppLogSink({
blockchainUrl: process.env.BLOCKCHAIN_BASE_URL!, // node01: http://infra01:6801
authorization: serviceSigner, // service-owned signature
proxyToken: process.env.PMATE_CHAIN_PROXY_TOKEN, // for HTTPS chain-proxy
}),
})createAppAccessLogger and withFetchAccess use this same Node logger queue.
They preserve server request IDs and allowlisted route templates. createAppLogWriter
provides acknowledged append/appendMany for audit/deployment records; failures
propagate instead of being silently queued. createAppAppendSink adapts an existing
transactional appender. Do not replace required audit acknowledgement with
best-effort telemetry. SDK stdout includes the original event ID and
logUpload: "application", so the collector does not upload it again.
Alternatively provide a custom sink(batch, signal); the sink must preserve item IDs/time, respect cancellation and report acceptance. The queue is bounded, attempts at most three times with the original event ID, reports failures without payloads, and does not block business work. close() stops admission and drains outstanding events; telemetry is best effort.
Counting
The envelope id is the event deduplication key. PV counts pv events; sessions deduplicate (appId, accessId); Profile UV deduplicates (appId, profileId) in a defined time window/topic. Missing identifiers are reported separately. Never count HTTP requests or vitals as page views, or infer first-ever visits from a limited retention window.
Error and crash diagnostics
Caught exceptions need explicit reporting; a global rejection listener cannot see
an exception already handled by application code. Use log.runtime.captureError(error,
{ operation: 'chat.send' }) on browser, React and Node loggers. Error records use
the error topic and include bounded message/stack diagnostics with URL queries
removed. Applications must redact domain-specific sensitive strings before
calling this method.
Inside LogProvider, wrap the application in LogErrorBoundary with a fallback
React node. It reports descendant render/lifecycle failures with component stacks.
Event handlers, asynchronous operations and errors above the boundary require
separate reporting. Explicitly allowlisted log.fetch requests report non-2xx and
network failures without bodies or request URLs.
Node exports installCrashReporter({ directory, send }). Install it before
starting the service. It monitors fatal exceptions without suppressing Node's
fatal exit, persists up to 20 records, and exposes replay() for startup recovery.
The send(entry) callback must resolve only after server acknowledgement, retain
entry ID/time, and route records to the service's crash topic. Failed uploads
remain on disk. Use a service-specific private persistent directory. It cannot
capture SIGKILL, OOM kills, native faults, or failures before installation.
