Files
twenty/packages/twenty-server/src/engine/core-modules/audit
Marie c13bc60dad Improve error handling (#13130)
In the BE we throw custom errors with precise error codes (e.g.
"LABEL_ALREADY_EXISTS") before catching them in filters and rethrowing
BaseGraphQLErrors (standard errors such as NotFoundError, UserInputError
etc.).
In the FE we were grouping sentries based on the error codes but we were
actually grouping by very broad codes such as "NOT_FOUND" or
"BAD_USER_INPUT", extracted from the BaseGraphQLErrors.

To fix that, we update the BaseGraphQLError constructor api to allow to
pass on the CustomError directly and retrieve from it the original code
and store it in existing property `subCode` that we will use in the FE
to send errors to sentry.
This new api also eases usage of `userFriendlyMessage` that is passed on
to the api response and therefore to the FE when CustomError is passed
on directly to the BaseGraphQLError constructor.
2025-07-09 17:13:44 +02:00
..
2025-05-07 09:39:18 +02:00
2025-05-07 09:39:18 +02:00

Analytics Module

This module provides analytics tracking functionality for the Twenty application.

Usage

Tracking Events

The AuditService provides a createContext method that returns an object with three methods:

  • insertWorkspaceEvent: For tracking workspace-level events
  • createObjectEvent: For tracking object-level events that include record and metadata IDs
  • createPageviewEvent: For tracking page views
import { Injectable } from '@nestjs/common';
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/custom-domain/custom-domain-activated';

@Injectable()
export class MyService {
  constructor(private readonly auditService: AuditService) {}

  async doSomething() {
    // Create an analytics context
    const analytics = this.auditService.createContext({
      workspaceId: 'workspace-id',
      userId: 'user-id',
    });

    // Track a workspace event
    analytics.insertWorkspaceEvent(CUSTOM_DOMAIN_ACTIVATED_EVENT, {});
    
    // Track an object event
    analytics.createObjectEvent(OBJECT_RECORD_CREATED_EVENT, {
      recordId: 'record-id',
      objectMetadataId: 'object-metadata-id',
      // other properties
    });
    
    // Track a pageview
    analytics.createPageviewEvent('page-name', {
      href: '/path',
      locale: 'en-US',
      // other properties
    });
  }
}

Adding New Events

To add a new event:

  1. Create a new file in the src/engine/core-modules/analytics/utils/events/track directory
  2. Define the event name, schema, and type
  3. Register the event using the registerEvent function
  4. Update the TrackEventName and TrackEventProperties types in src/engine/core-modules/analytics/utils/events/event-types.ts

Example:

// src/engine/core-modules/analytics/utils/events/track/my-feature/my-event.ts
import { z } from 'zod';

import { registerEvent } from 'src/engine/core-modules/analytics/utils/events/track/track';

export const MY_EVENT = 'My Event' as const;
export const myEventSchema = z.object({
  event: z.literal(MY_EVENT),
  properties: z.object({
    myProperty: z.string(),
  }),
});

export type MyEventTrackEvent = z.infer<typeof myEventSchema>;

registerEvent(MY_EVENT, myEventSchema);

Then update the events.type.ts file:

// src/engine/core-modules/analytics/types/events.type.ts
import { MY_EVENT, MyEventTrackEvent } from '../utils/events/track/my-feature/my-event';

// Add to the union type
export type TrackEventName = 
  | typeof MY_EVENT
  // ... other event names;

// Add to the TrackEvents interface
export interface TrackEvents {
  [MY_EVENT]: MyEventTrackEvent;
  // ... other event types
}

// The TrackEventProperties type will automatically use the new event
export type TrackEventProperties<T extends TrackEventName> = T extends keyof TrackEvents
  ? TrackEvents[T]['properties']
  : object;

API

AuditService

createContext(context?)

Creates an analytics context with the given user ID and workspace ID.

  • context (optional): An object with userId and workspaceId properties

Returns an object with the following methods:

  • insertWorkspaceEvent<T extends TrackEventName>(event: T, properties: TrackEventProperties<T>): Tracks a workspace-level event
  • createObjectEvent<T extends TrackEventName>(event: T, properties: TrackEventProperties<T> & { recordId: string; objectMetadataId: string }): Tracks an object-level event
  • createPageviewEvent(name: string, properties: Partial<PageviewProperties>): Tracks a pageview

Types

TrackEventName

A union type of all registered event names, plus string for backward compatibility.

TrackEventProperties

A mapped type that maps each event name to its corresponding properties type. It uses the TrackEvents interface to provide a more maintainable and type-safe way to map event names to their properties.

// Define the mapping between event names and their event types
export interface TrackEvents {
  [EVENT_NAME_1]: Event1Type;
  [EVENT_NAME_2]: Event2Type;
  // ... other event types
}

// Use the mapping to extract properties for each event type
export type TrackEventProperties<T extends TrackEventName> = T extends keyof TrackEvents
  ? TrackEvents[T]['properties']
  : object;

This approach makes it easier to add new events without having to modify a complex nested conditional type.

PageviewProperties

Properties for pageview events, including href, locale, pathname, referrer, sessionId, timeZone, and userAgent.