Two challenges with error messages - always provide a useful/meaningful error message for the end user instead of the generic one. eg: show "Wrong password" and not "An error occured" - avoid technical details unless error regards a technical feature. eg: show "An error occured" and not "Invalid post-hook payload."; but do show "Invalid issuer URL." as it occurs while configuring SSO What this PR does - Make userFriendlyMessage mandatory for widely used GraphqlQueryRunnerException and CommonQueryRunnerException, so that developers are forced to ask themselves what the error message should be, and as it contains very wide error codes (eg: "Bad request") which should not be mapped to just one default message - Keep userFriendlyMessage optional for service-specific exceptions (eg: workflowStepExecutorException), but convert the error code to userFriendlyMessage mapper to a switch case function with a typecheck ensuring that all codes are mapped to a message. These default messages are still overridable where they are thrown.
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 auditService = this.auditService.createContext({
workspaceId: 'workspace-id',
userId: 'user-id',
});
// Track a workspace event
auditService.insertWorkspaceEvent(CUSTOM_DOMAIN_ACTIVATED_EVENT, {});
// Track an object event
auditService.createObjectEvent(OBJECT_RECORD_CREATED_EVENT, {
recordId: 'record-id',
objectMetadataId: 'object-metadata-id',
// other properties
});
// Track a pageview
auditService.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.