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

bash
npm install @aj-2000-test/goodlogs-sdk

Quick Start

Node.js / Express

typescript
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)

typescript
// 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)

typescript
// 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)

typescript
// 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)

html
<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

OptionTypeDefaultDescription
apiKeystringRequiredAPI key — gl_sk_* (logs + events) or gl_pk_* (events only)
endpointstringAutoCustom API URL. Auto-resolved from key region prefix
batchSizenumber50Items buffered before triggering a flush
flushIntervalnumber5000Milliseconds between automatic flushes
defaultContextobject{}Key-value pairs merged into every log and event
onErrorfunctionno-opCallback for flush errors: (error: Error) => void
disabledbooleanfalseDisable all SDK functionality (useful in tests)
telemetrybooleantrueSend anonymous SDK diagnostics
autocapturebooleantrue*Auto-capture clicks, pageviews, Web Vitals (* browser only)
useEnvelopebooleanfalseUse the unified /v1/envelope pipeline (enables errors, traces, vitals). Will become the default once the legacy endpoints retire.
autoFetchbooleantrue when useEnvelope is onPatch globalThis.fetch so every outgoing call becomes an http.client span and injects W3C traceparent
releasestringundefinedRelease version (e.g. v1.4.2) attached to errors + spans for regression detection
environmentstringundefinedEnvironment tag (e.g. production, staging)
servicestringundefinedService 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.

typescript
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

typescript
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

typescript
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
typescript
// 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:

typescript
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.

typescript
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.

typescript
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?)

typescript
gl.log("error", "Database connection timeout", {
  host: "db-primary.internal",
  timeout_ms: 5000,
  retries: 3,
  service: "user-service",
  env: "production",
});
OptionTypeDescription
propertiesobjectAny 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

typescript
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.

typescript
// 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",
});
PropertyTypeDescription
distinctIdstringReserved — user ID for analytics (DAU, retention). Stored as distinct_id.
(any key)anyAll other properties are stored flat and searchable via GQL
timestampDateOverride 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

typescript
// 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");
MethodDescription
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.

bash
Event: $pageview
Properties: $url, $path, $referrer, $title

Click Tracking

Captures clicks on links, buttons, [role="button"], and [data-gl-event] elements.

wasm
Event: $click (or data-gl-event attribute value)
Properties: $element_tag, $element_text, $element_href, $element_classes

Web Vitals

MetricDescription
FCPFirst Contentful Paint
LCPLargest Contentful Paint
CLSCumulative Layout Shift
TTFBTime to First Byte
INPInteraction to Next Paint
bash
Event: $web_vital
Properties: $metric, $value_ms or $value

Flushing & Shutdown

Flushes happen automatically on batch size, timer, or page hide. For explicit control:

typescript
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

typescript
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

ScenarioBehavior
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 errorRequeues items, retries on next flush
Partial rejectionAccepted items processed; rejected items requeued

Auto-Injected Fields

FieldSourceExample
$session_idAuto-generated"ses_abc123"
$distinct_ididentify() or auto"user_123" or "anon_xyz"
$goodlogs_sdkSDK type"js"
$goodlogs_sdk_versionBuild-time"0.1.18"
$urlBrowser only"https://example.com/dash"
$pathBrowser only"/dash"
$referrerBrowser only"https://google.com"
$screenBrowser only"1920x1080"
$browserServer-side (UA)"Chrome 125"
$osServer-side (UA)"macOS 14"
$deviceServer-side (UA)"Desktop"
$ipServer-side"203.0.113.42"

API Key Types

Key TypeFormatPermissions
Publicgl_pk_{region}_...Events only (track). Safe for client-side.
Secretgl_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

typescript
// 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.

typescript
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.

typescript
// 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.

typescript
// 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:

typescript
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

typescript
// 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

typescript
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

typescript
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

typescript
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

typescript
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})`);
  }
}