JavaScript / TypeScript SDK
The @aj-2000-test/goodlogs-sdk works in Node.js, Next.js, React, and vanilla browser JavaScript. It handles batching, retries, auto-capture, and graceful shutdown automatically.
Installation
npm install @aj-2000-test/goodlogs-sdk
Quick Start
Node.js / Express
import { GoodLogs } from "@aj-2000-test/goodlogs-sdk";
const gl = new GoodLogs({
apiKey: "gl_sk_eu_your_secret_key",
defaultContext: { service: "api", env: "production" },
});
// Log errors
gl.error("Payment failed", {
orderId: "ord_123",
errorCode: "card_declined",
});
// Track events
gl.track("purchase", {
distinctId: "user_456",
plan: "pro",
amount: 29,
});
// Flush before process exit
process.on("SIGTERM", async () => {
await gl.shutdown();
process.exit(0);
});
Next.js (App Router — Server Side)
// lib/goodlogs.ts
import { GoodLogs } from "@aj-2000-test/goodlogs-sdk";
let gl: GoodLogs | null = null;
export function getGoodLogs() {
if (!gl) {
gl = new GoodLogs({
apiKey: process.env.GOODLOGS_API_KEY!,
defaultContext: { service: "web", env: process.env.NODE_ENV },
});
}
return gl;
}
// In a Server Action or API Route:
export async function createOrder(data: FormData) {
"use server";
const gl = getGoodLogs();
try {
const order = await processOrder(data);
gl.track("purchase", {
distinctId: order.userId,
amount: order.total,
plan: order.plan,
});
return order;
} catch (err) {
gl.error("Order failed", { error: String(err) });
throw err;
}
}
Next.js (Client-Side with Auto-Capture)
// components/analytics-provider.tsx
"use client";
import { useEffect, useRef } from "react";
import { GoodLogs } from "@aj-2000-test/goodlogs-sdk";
export function AnalyticsProvider({ children }: { children: React.ReactNode }) {
const glRef = useRef<GoodLogs | null>(null);
useEffect(() => {
glRef.current = new GoodLogs({
apiKey: "gl_pk_eu_your_public_key",
autocapture: true, // Captures clicks, pageviews, Web Vitals
});
return () => { glRef.current?.shutdown(); };
}, []);
return <>{children}</>;
}
// app/layout.tsx
import { AnalyticsProvider } from "@/components/analytics-provider";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<AnalyticsProvider>{children}</AnalyticsProvider>
</body>
</html>
);
}
React (Vite / CRA)
// src/goodlogs.ts
import { GoodLogs } from "@aj-2000-test/goodlogs-sdk";
export const gl = new GoodLogs({
apiKey: "gl_pk_eu_your_public_key",
autocapture: true,
});
// src/App.tsx
import { gl } from "./goodlogs";
function CheckoutButton({ plan, userId }: { plan: string; userId: string }) {
const handleClick = () => {
gl.track("checkout_started", {
distinctId: userId,
plan,
});
};
return <button onClick={handleClick}>Upgrade to {plan}</button>;
}
Vanilla JavaScript (Browser)
<script type="module">
import { GoodLogs } from "https://esm.sh/@aj-2000-test/goodlogs-sdk";
const gl = new GoodLogs({
apiKey: "gl_pk_eu_your_public_key",
autocapture: true,
});
document.getElementById("signup-btn").addEventListener("click", () => {
gl.track("signup_clicked", {
properties: { page: window.location.pathname },
});
});
</script>
<!-- data-gl-event attribute for zero-JS click tracking -->
<button data-gl-event="add_to_cart">Add to Cart</button>
<a href="/pricing" data-gl-event="view_pricing">View Pricing</a>
Constructor Options
| Option | Type | Default | Description |
|---|---|---|---|
| apiKey | string | Required | API key — gl_sk_* (logs + events) or gl_pk_* (events only) |
| endpoint | string | Auto | Custom API URL. Auto-resolved from key region prefix |
| batchSize | number | 50 | Items buffered before triggering a flush |
| flushInterval | number | 5000 | Milliseconds between automatic flushes |
| defaultContext | object | {} | Key-value pairs merged into every log and event |
| onError | function | no-op | Callback for flush errors: (error: Error) => void |
| disabled | boolean | false | Disable all SDK functionality (useful in tests) |
| telemetry | boolean | true | Send anonymous SDK diagnostics |
| autocapture | boolean | true* | Auto-capture clicks, pageviews, Web Vitals (* browser only) |
| useEnvelope | boolean | false | Use the unified /v1/envelope pipeline (enables errors, traces, vitals). Will become the default once the legacy endpoints retire. |
| autoFetch | boolean | true when useEnvelope is on | Patch globalThis.fetch so every outgoing call becomes an http.client span and injects W3C traceparent |
| release | string | undefined | Release version (e.g. v1.4.2) attached to errors + spans for regression detection |
| environment | string | undefined | Environment tag (e.g. production, staging) |
| service | string | undefined | Service name attached to spans (service.name in OTel terms) |
Tip: Region is auto-detected from your API key: gl_sk_eu_... → EU API, gl_sk_ap_... → Asia-Pacific.
Errors
When useEnvelope: true is set, the SDK captures exceptions and groups them into Issues.
try {
await chargeCard()
} catch (e) {
gl.captureException(e, {
tags: { feature: "checkout" },
fingerprint: ["payment-flow"], // optional explicit grouping
user_id: currentUserId,
})
}
// Freeform message
gl.captureMessage("hit unreachable branch", { level: "warning" })
// Rolling breadcrumb trail (max 50) — attached to the next captureException.
gl.addBreadcrumb({ category: "ui.click", message: "Buy" })
gl.addBreadcrumb({ category: "fetch", message: "GET /api/cart" })
Stack traces are parsed best-effort (V8 + Spidermonkey + WebKit formats). Frames outside node_modules/ and /dist/ are marked in_app: true and highlighted in the dashboard.
See Errors & Issues for the full lifecycle, fingerprinting, and the zero-config log-to-issue scanner.
Tracing
const tx = gl.startTransaction({ name: "/checkout", op: "http.server" })
tx.setAttribute("user.id", userId)
const dbSpan = tx.startChild({ op: "db.query", name: "SELECT users" })
dbSpan.addEvent("query.start")
dbSpan.setStatus("ok")
dbSpan.finish()
tx.finish()
autoFetch (default true when useEnvelope is on) wraps every browser fetch() as a span automatically — including W3C traceparent injection. See Tracing & Performance for the full data model.
Node framework adapters
import express from "express"
import { GoodLogs, goodlogsExpress, instrumentNodeHttp } from "@aj-2000-test/goodlogs-sdk"
const gl = new GoodLogs({ apiKey: "gl_sk_...", useEnvelope: true })
instrumentNodeHttp(gl) // outbound http / https outbound
const app = express()
app.use(goodlogsExpress(gl)) // inbound — http.server spans
// Fastify
import { goodlogsFastify } from "@aj-2000-test/goodlogs-sdk"
app.register(goodlogsFastify(gl))
// NestJS
import { goodlogsNestInterceptor } from "@aj-2000-test/goodlogs-sdk"
{ provide: APP_INTERCEPTOR, useValue: goodlogsNestInterceptor(gl) }
All three honour incoming W3C traceparent for multi-service traces.
Web Vitals
Auto-captured in the browser via PerformanceObserver. To send one manually:
gl.captureVital("lcp", 2300, { route: "/checkout" })
rating is computed locally using web.dev thresholds before sending. See Web Vitals.
Session Replay
Separate subpath import — doesn't bloat your core bundle. Requires rrweb as a peer dependency.
import { startReplay } from "@aj-2000-test/goodlogs-sdk/replay"
const handle = await startReplay(gl, {
sampleRate: 1, // record 100% of sessions
maskAllInputs: true, // mask <input>/<textarea> text
sessionTimeoutMs: 30 * 60 * 1000, // 30 min inactivity → rotate
})
// handle.stop() to end, handle.flush() to force-flush
Returns null on the server (SSR) or if sampling excludes this session. See Session Replay for full options, lifecycle, and privacy controls.
Continuous Profiling
Uses the JS Self-Profiling API (Chromium 94+). Returns null when unavailable.
import { startProfiling } from "@aj-2000-test/goodlogs-sdk/profiling"
const handle = await startProfiling(gl, {
service: "my-app",
chunkIntervalMs: 60_000,
})
// handle.stop() to end profiling
See Profiling for full options and chunk format.
Logging
gl.log(severity, message, properties?)
gl.log("error", "Database connection timeout", {
host: "db-primary.internal",
timeout_ms: 5000,
retries: 3,
service: "user-service",
env: "production",
});
| Option | Type | Description |
|---|---|---|
| properties | object | Any key-value data — all fields are searchable via GQL |
Tip: Just pass any properties you want. They're all stored flat and searchable. No need for metadata/context wrappers.
Severity Shortcuts
gl.debug("Cache hit", { key: "user:123" });
gl.info("User signed up", { userId: "u_456" });
gl.warn("Rate limit approaching", { current: 95, limit: 100 });
gl.error("Payment failed", { orderId: "ord_789" });
gl.fatal("Process crashed", { signal: "SIGKILL" });
In Node.js, the calling function name, file, and line number are auto-captured from the stack trace.
Event Tracking
gl.track(event, properties?)
Track a product analytics event. Pass any properties as a flat object.
// Basic event
gl.track("page_view");
// With properties
gl.track("purchase", {
plan: "pro",
amount: 29.99,
currency: "USD",
});
// With user identity — distinctId is a reserved key
gl.track("signup", {
distinctId: "user_123",
method: "google",
referrer: "product_hunt",
});
| Property | Type | Description |
|---|---|---|
| distinctId | string | Reserved — user ID for analytics (DAU, retention). Stored as distinct_id. |
| (any key) | any | All other properties are stored flat and searchable via GQL |
| timestamp | Date | Override timestamp (default: now) |
Tip: Pass distinctId in the properties object — it's automatically extracted and sent as the user identifier. Query it in GQL as distinct_id.
Identity Management
// After login — link anonymous activity to real user
gl.identify("user_123");
// Get current ID (anonymous or identified)
gl.getDistinctId(); // "user_123" or "anon_abc..."
// On logout — reset to new anonymous ID
gl.reset();
// Session management
gl.getSessionId();
gl.newSession();
gl.setSessionId("custom_session_id");
| Method | Description |
|---|---|
| identify(userId) | Replace anonymous ID with real user ID. Persists in localStorage. |
| getDistinctId() | Get current distinct ID (user or anonymous) |
| reset() | Generate new anonymous + session IDs (call on logout) |
| getSessionId() | Get current session ID |
| newSession() | Start a new session with fresh ID |
| setSessionId(id) | Set an explicit session ID |
Auto-Capture (Browser)
When autocapture: true (default in browsers), the SDK automatically captures:
Page Views
SPA-aware — detects route changes via History API, popstate, and URL polling. Works with React Router, Next.js, Vue Router.
Event: $pageview
Properties: $url, $path, $referrer, $title
Click Tracking
Captures clicks on links, buttons, [role="button"], and [data-gl-event] elements.
Event: $click (or data-gl-event attribute value)
Properties: $element_tag, $element_text, $element_href, $element_classes
Web Vitals
| Metric | Description |
|---|---|
| FCP | First Contentful Paint |
| LCP | Largest Contentful Paint |
| CLS | Cumulative Layout Shift |
| TTFB | Time to First Byte |
| INP | Interaction to Next Paint |
Event: $web_vital
Properties: $metric, $value_ms or $value
Flushing & Shutdown
Flushes happen automatically on batch size, timer, or page hide. For explicit control:
await gl.flush(); // Send all buffered data now
await gl.shutdown(); // Flush + stop timers (graceful cleanup)
Warning: Always call gl.shutdown() or gl.flush() before process exit in Node.js, Lambda, or serverless.
AWS Lambda / Serverless
const gl = new GoodLogs({
apiKey: process.env.GOODLOGS_API_KEY!,
batchSize: 1, // Flush immediately
flushInterval: 1000, // Short interval
});
export const handler = async (event: any) => {
gl.info("Lambda invoked", { requestId: event.requestContext?.requestId });
try {
return await processEvent(event);
} catch (err) {
gl.error("Lambda failed", { error: String(err) });
throw err;
} finally {
await gl.flush(); // Critical: flush before freeze
}
};
Retry & Error Handling
| Scenario | Behavior |
|---|---|
| Rate limit (429) | Exponential backoff: 1s → 2s → 4s → 8s + jitter, max 4 retries |
| Server overload (503) | Same exponential backoff |
| Server error (5xx) | Requeues items for next flush |
| Client error (4xx) | Logs error, does not retry |
| Network error | Requeues items, retries on next flush |
| Partial rejection | Accepted items processed; rejected items requeued |
Auto-Injected Fields
| Field | Source | Example |
|---|---|---|
| $session_id | Auto-generated | "ses_abc123" |
| $distinct_id | identify() or auto | "user_123" or "anon_xyz" |
| $goodlogs_sdk | SDK type | "js" |
| $goodlogs_sdk_version | Build-time | "0.1.18" |
| $url | Browser only | "https://example.com/dash" |
| $path | Browser only | "/dash" |
| $referrer | Browser only | "https://google.com" |
| $screen | Browser only | "1920x1080" |
| $browser | Server-side (UA) | "Chrome 125" |
| $os | Server-side (UA) | "macOS 14" |
| $device | Server-side (UA) | "Desktop" |
| $ip | Server-side | "203.0.113.42" |
API Key Types
| Key Type | Format | Permissions |
|---|---|---|
| Public | gl_pk_{region}_... | Events only (track). Safe for client-side. |
| Secret | gl_sk_{region}_... | Logs + events. Server-side only. |
Warning: Never use secret keys (gl_sk_*) in client-side code. Use public keys for browser tracking.
TypeScript Types
// Ingest types
import type {
GoodLogsOptions,
Severity, // "debug" | "info" | "warn" | "error" | "fatal"
LogEntry, // { severity, message, properties?, timestamp? }
EventEntry, // { event, distinctId?, properties?, timestamp? }
} from "@aj-2000-test/goodlogs-sdk";
// API client types (responses, requests)
import type {
GoodLogsClientOptions,
LogRecord,
GqlResult, GqlBatchResult, GqlAutocompleteResult, Nl2GqlResult,
SchemaResponse,
AlertRule, CreateAlertRule, UpdateAlertRule,
SloDefinition, CreateSlo, UpdateSlo,
AiChatResponse, ProjectInfo,
} from "@aj-2000-test/goodlogs-sdk";
Programmatic API Client
The GoodLogsClient provides typed access to all read/write endpoints. Requires a secret API key.
import { GoodLogsClient } from "@aj-2000-test/goodlogs-sdk";
const client = new GoodLogsClient({ apiKey: "gl_sk_us_..." });
Query Logs
Use GQL to query logs — it supports filtering, full-text search, aggregation, and time ranges.
// Search error logs from the last hour
const result = await client.gql("severity:error last:1h");
for (const row of result.data as any[]) {
console.log(`[${row.severity}] ${row.message}`);
}
// Full-text search
const search = await client.gql("'payment failed' last:24h limit:20");
// Count errors by message
const grouped = await client.gql("count by message | where severity = 'error' | last 1h");
Run GQL Queries
All data querying uses POST /v1/gql — one endpoint for everything.
// Single query
const result = await client.gql("count | where severity = 'error' | last 1h");
console.log(result.data);
// Natural language → GQL
const { gql } = await client.nl2gql("Show me the most common errors today");
const data = await client.gql(gql);
Batch Queries
Fetch multiple metrics in a single request:
const [dau, total, topEvents] = await client.gqlBatch([
"from:events | count_distinct(distinct_id) | last:1d",
"from:events | count | last:30d",
"from:events | count by event_name | top 10 | last:30d",
]);
// Each result: { status: "ok", query, data } or { status: "error", error: "msg" }
if (dau.status === "ok") console.log("DAU:", dau.data);
Alerts
// List alerts
const alerts = await client.alerts.list();
// Create alert
const alert = await client.alerts.create({
name: "High Error Rate",
metric: "error_count",
threshold: 100,
window_minutes: 15,
severity: "critical",
webhook_url: "https://hooks.slack.com/...",
});
// Update alert
await client.alerts.update(alert.id, { threshold: 200, enabled: true });
// Mute for 1 hour
await client.alerts.mute(alert.id, 60);
// Alert timeline via GQL
const timeline = await client.gql("from:alert_events | last 7d | sort triggered_at desc | limit 20");
// Delete
await client.alerts.delete(alert.id);
SLOs
const slo = await client.slos.create({
name: "API Availability",
target_percent: 99.9,
window_days: 30,
});
const slos = await client.slos.list();
await client.slos.update(slo.id, { target_percent: 99.95 });
await client.slos.delete(slo.id);
AI Chat
const response = await client.ai.chat("Why are error rates spiking?");
console.log(response.reply);
// Continue conversation
const followup = await client.ai.chat("Show me the specific errors", response.session_id);
Schema & Info
const schema = await client.schema();
console.log("Event types:", schema.events.map(e => e.name));
const info = await client.info();
console.log(`Project: ${info.project.name}, Plan: ${info.org.plan}`);
Error Handling
import { GoodLogsClient, GoodLogsApiError } from "@aj-2000-test/goodlogs-sdk";
try {
await client.alerts.create({ name: "Test", metric: "x", threshold: 0 });
} catch (err) {
if (err instanceof GoodLogsApiError) {
console.error(`API error ${err.status}: ${err.message} (${err.code})`);
}
}