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.
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 eventscreateObjectEvent: For tracking object-level events that include record and metadata IDscreatePageviewEvent: 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:
- Create a new file in the
src/engine/core-modules/analytics/utils/events/trackdirectory - Define the event name, schema, and type
- Register the event using the
registerEventfunction - Update the
TrackEventNameandTrackEventPropertiestypes insrc/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 withuserIdandworkspaceIdproperties
Returns an object with the following methods:
insertWorkspaceEvent<T extends TrackEventName>(event: T, properties: TrackEventProperties<T>): Tracks a workspace-level eventcreateObjectEvent<T extends TrackEventName>(event: T, properties: TrackEventProperties<T> & { recordId: string; objectMetadataId: string }): Tracks an object-level eventcreatePageviewEvent(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.