Skip to main content
PostHogAnalytics is the Analytics component’s PostHog backend. It wraps the official posthog-node client to capture product events from the server, attaching a distinct user id, event properties, and optional groups. It implements the framework’s IAnalytics interface, so call sites stay provider-agnostic and you can swap the backend without touching the code that emits events.

Why PostHog

  • Server-side capture. Send events directly from your backend with posthog-node — no client SDK required.
  • Typed capture options. capture() takes a typed payload: distinct id, event name, properties, and optional groups.
  • EU or US hosting. Defaults to the EU cloud and is configurable for US cloud or a self-hosted instance.
  • Graceful shutdown. shutdown() flushes buffered events before the process exits.
  • Container-managed. Registered with @decorator.analytics() and resolved from the container.

Installation

PostHogAnalytics ships with @ooneex/analytics and depends on the PostHog Node client.
bun add @ooneex/analytics posthog-node

Environment variables

VariableRequiredPurpose
ANALYTICS_POSTHOG_PROJECT_TOKENYesPostHog project API key. Missing throws AnalyticsException (API_KEY_REQUIRED).
ANALYTICS_POSTHOG_HOSTNoPostHog instance host. Defaults to https://eu.i.posthog.com.
ANALYTICS_POSTHOG_PROJECT_TOKEN=phc_xxxxxxxxxxxxxxxxxxxxxxxx
# Use the US cloud or a self-hosted instance instead of the EU default:
ANALYTICS_POSTHOG_HOST=https://us.i.posthog.com
The project token is validated when PostHogAnalytics is constructed, so a missing token fails fast at startup.

How it works

On construction, the backend creates a single PostHog client pointed at your host. Each capture() call maps your payload onto a PostHog event: the id becomes the distinctId, properties are sent under $set, the current timestamp is attached, and any groups are forwarded. Capture is fire-and-forget — events are buffered by the client and flushed in the background.
MethodPurpose
capture(options)Queue an event for the given distinct id.
shutdown()Flush buffered events and close the client.
The capture payload:
FieldTypePurpose
idstringDistinct id of the user or entity the event belongs to.
eventstringEvent name, e.g. "order_completed".
propertiesRecord<string, unknown>Optional event properties; sent as $set.
groupsRecord<string, string | number>Optional group analytics keys.

Usage

import { container } from "@ooneex/container";
import { PostHogAnalytics } from "@ooneex/analytics";

const analytics = container.get(PostHogAnalytics);

// Capture an event
analytics.capture({
  id: "user_123",
  event: "order_completed",
  properties: {
    orderId: "ord_456",
    total: 4200,
    currency: "EUR",
  },
});

// Group analytics
analytics.capture({
  id: "user_123",
  event: "feature_used",
  properties: { feature: "export" },
  groups: { company: "acme_inc" },
});

// Flush before the process exits
await analytics.shutdown();
Inject it into a service to record events as part of your domain logic:
import { inject } from "@ooneex/container";
import { PostHogAnalytics } from "@ooneex/analytics";

export class CheckoutService {
  constructor(
    @inject(PostHogAnalytics) private readonly analytics: PostHogAnalytics,
  ) {}

  public async completeOrder(userId: string, order: Order) {
    // ... persist the order ...
    this.analytics.capture({
      id: userId,
      event: "order_completed",
      properties: { orderId: order.id, total: order.total },
    });
  }
}

Use in the app

In an @ooneex/app application, analytics isn’t a dedicated App config slot — PostHogAnalytics registers itself with the container through @decorator.analytics() as soon as the class is imported, and you resolve or inject it wherever you record events.
bun add @ooneex/app @ooneex/analytics posthog-node
Inject it into a service or controller and capture events as part of your domain logic:
import { inject } from "@ooneex/container";
import { PostHogAnalytics } from "@ooneex/analytics";

export class CheckoutService {
  constructor(
    @inject(PostHogAnalytics) private readonly analytics: PostHogAnalytics,
  ) {}

  public async completeOrder(userId: string, order: Order) {
    // ... persist the order ...
    this.analytics.capture({
      id: userId,
      event: "order_completed",
      properties: { orderId: order.id, total: order.total },
    });
  }
}
Because capture is buffered, flush the client before the process exits — for example from a shutdown hook or an onStart handler that registers one:
import { container } from "@ooneex/container";
import { PostHogAnalytics } from "@ooneex/analytics";

process.on("SIGTERM", async () => {
  await container.get(PostHogAnalytics).shutdown();
  process.exit(0);
});
Set ANALYTICS_POSTHOG_PROJECT_TOKEN (and optionally ANALYTICS_POSTHOG_HOST) in your .env.yml (or environment) so the client can initialize.

Exceptions

PostHogAnalytics throws AnalyticsException on misconfiguration, carrying a machine-readable key.
KeyWhen
API_KEY_REQUIREDPostHogAnalytics is constructed without ANALYTICS_POSTHOG_PROJECT_TOKEN.
import { AnalyticsException } from "@ooneex/analytics";

try {
  const analytics = container.get(PostHogAnalytics);
} catch (error) {
  if (error instanceof AnalyticsException && error.key === "API_KEY_REQUIRED") {
    logger.error("PostHog is missing its project token", error.data);
  }
  throw error;
}

Best practices

  • Call shutdown() on exit. Capture is buffered; flush before the process terminates so you don’t drop the last events.
  • Use stable distinct ids. Pass a consistent id per user or entity so events stitch together into one timeline.
  • Name events consistently. Use a convention like noun_verb (order_completed, user_signed_up) so they’re easy to query.
  • Keep the project token in the environment. Load it from .env; never hard-code it.
  • Match the host to your data region. Override ANALYTICS_POSTHOG_HOST for the US cloud or a self-hosted instance — the default is the EU cloud.
See the Analytics component for the provider interface and how events flow through the framework.