PostHog / posthog-js-lite

Reimplementation of posthog-js to be as light and modular as possible.
https://posthog.com/docs/libraries
MIT License
70 stars 36 forks source link

Sentry Integration for `posthog-node` #106

Closed huw closed 11 months ago

huw commented 1 year ago

I love the PostHog Sentry integration on the client-side, so I rewrote it for posthog-node:

import {
  type Event,
  type EventProcessor,
  type Exception,
  type Hub,
  type Integration,
  type Primitive,
} from "@sentry/types";
import { type PostHog } from "posthog-node";

interface PostHogSentryExceptionProperties {
  $sentry_event_id?: string;
  $sentry_exception?: { values?: Exception[] };
  $sentry_exception_message?: string;
  $sentry_exception_type?: string;
  $sentry_tags: { [key: string]: Primitive };
  $sentry_url?: string;
  $exception_type?: string;
  $exception_message?: string;
  $exception_source?: string;
  $exception_lineno?: number;
  $exception_colno?: number;
  $exception_DOMException_code?: string;
  $exception_is_synthetic?: boolean;
  $exception_stack_trace_raw?: string;
  $exception_handled?: boolean;
  $exception_personURL?: string;
}

export class PostHogLink implements Integration {
  public readonly name = "posthog-node";

  public constructor(
    private readonly posthog: PostHog,
    private readonly posthogHost: string,
    private readonly organization?: string,
    private readonly prefix?: string
  ) {}

  public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub) {
    addGlobalEventProcessor((event: Event) => {
      if (event.exception?.values === undefined || event.exception.values.length === 0) {
        return event;
      }

      if (!event.tags) {
        event.tags = {};
      }

      const sentry = getCurrentHub();
      const userId = sentry.getScope().getUser()?.id?.toString();

      if (userId === undefined) {
        // If we can't find a user ID, don't bother linking the event. We won't be able to send anything meaningful to PostHog without it.
        return event;
      }

      event.tags["PostHog Person URL"] = new URL(`/person/${userId}`, this.posthogHost).toString();

      const properties: PostHogSentryExceptionProperties = {
        // PostHog Exception Properties
        $exception_message: event.exception.values[0]?.value,
        $exception_type: event.exception.values[0]?.type,
        $exception_personURL: event.tags["PostHog Person URL"],
        // Sentry Exception Properties
        $sentry_event_id: event.event_id,
        $sentry_exception: event.exception,
        $sentry_exception_message: event.exception.values[0]?.value,
        $sentry_exception_type: event.exception.values[0]?.type,
        $sentry_tags: event.tags,
      };

      const projectId = sentry.getClient()?.getDsn()?.projectId;
      if (this.organization !== undefined && projectId !== undefined && event.event_id !== undefined) {
        properties.$sentry_url = new URL(
          `${this.organization}/issues/?project=${projectId}&query=${event.event_id}`,
          this.prefix ?? "https://sentry.io/organizations"
        ).toString();
      }

      this.posthog.capture({ event: "$exception", distinctId: userId, properties });

      return event;
    });
  }
}

I’d submit this as a PR, but it does have one small caveat—because posthog-node doesn’t store a persistent distinctId, we have to find it from somewhere. In my case, I keep the distinctId and the Sentry user.id in sync (with sentry.setUser({ id }) earlier in my code), but this obviously isn’t going to work for everyone. I am not entirely sure what a more robust solution would look like here, but it felt worth sharing what I had as a starting point.

neilkakkar commented 1 year ago

This is great thank you! Happy to accept a PR for this.

Regarding where the user ID comes from, we have an integration in python as well where we add it as a middleware: https://github.com/PostHog/posthog-python/blob/master/posthog/sentry/posthog_integration.py#L25

I think as long as this integration has a mechanism for setting the id that's clearly explained & easy to setup, we're all good.