Add receipt metrics and logs to connected account sync webhooks (#22853)

Webhook deliveries from Google and Microsoft were invisible at the app
level: successful notifications produced no logs and no metrics, so
webhook-triggered syncs could not be told apart from cron polling.

Add two counters, connected-account-sync-webhook/received/messaging and
/received/calendar, mirroring the sync-job metric umbrellas, and log a
line whenever a notification triggers a sync. Unmatched subscriptions
keep their existing warn logs.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22853?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
neo773
2026-07-13 17:58:21 +05:30
committed by GitHub
parent 1b168ac1f7
commit 9f5d17d1f0
7 changed files with 76 additions and 2 deletions
@@ -53,4 +53,6 @@ export enum MetricsKeys {
AiChatTurnCompleted = 'ai-chat/turn-completed',
AiChatTurnFailed = 'ai-chat/turn-failed',
WorkspaceMetadataCacheLocalEviction = 'workspace-metadata-cache/local-eviction',
ConnectedAccountSyncWebhookReceivedMessaging = 'connected-account-sync-webhook/received/messaging',
ConnectedAccountSyncWebhookReceivedCalendar = 'connected-account-sync-webhook/received/calendar',
}
@@ -1,6 +1,6 @@
import { timingSafeEqual } from 'crypto';
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isNonEmptyString } from '@sniptt/guards';
@@ -8,6 +8,8 @@ import { WebhookSubscriptionStatus } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
import { WebhookSyncTriggerService } from 'src/modules/connected-account/webhook-subscription-manager/services/webhook-sync-trigger.service';
import {
@@ -21,13 +23,21 @@ const GOOGLE_CALENDAR_SYNC_RESOURCE_STATE = 'sync';
@Injectable()
export class GoogleCalendarNotificationHandler implements WebhookNotificationHandler<GoogleCalendarChannelNotification> {
private readonly logger = new Logger(GoogleCalendarNotificationHandler.name);
constructor(
@InjectRepository(CalendarChannelEntity)
private readonly calendarChannelRepository: Repository<CalendarChannelEntity>,
private readonly webhookSyncTriggerService: WebhookSyncTriggerService,
private readonly metricsService: MetricsService,
) {}
async handle(request: GoogleCalendarChannelNotification): Promise<void> {
this.metricsService.incrementCounterBy({
key: MetricsKeys.ConnectedAccountSyncWebhookReceivedCalendar,
amount: 1,
});
if (request.resourceState === GOOGLE_CALENDAR_SYNC_RESOURCE_STATE) {
return;
}
@@ -44,6 +54,10 @@ export class GoogleCalendarNotificationHandler implements WebhookNotificationHan
});
if (!isDefined(calendarChannel)) {
this.logger.warn(
`No calendar channel found for Google channel ${request.channelId}`,
);
return;
}
@@ -72,5 +86,9 @@ export class GoogleCalendarNotificationHandler implements WebhookNotificationHan
calendarChannel.id,
calendarChannel.workspaceId,
);
this.logger.log(
`Triggered calendar sync for calendar channel ${calendarChannel.id} from Google notification`,
);
}
}
@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isNonEmptyString } from '@sniptt/guards';
@@ -10,6 +10,8 @@ import {
import { isDefined } from 'twenty-shared/utils';
import { In, Repository } from 'typeorm';
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
@@ -31,8 +33,11 @@ export type GoogleMessagingNotificationRequest = {
@Injectable()
export class GoogleMessagingNotificationHandler implements WebhookNotificationHandler<GoogleMessagingNotificationRequest> {
private readonly logger = new Logger(GoogleMessagingNotificationHandler.name);
constructor(
private readonly twentyConfigService: TwentyConfigService,
private readonly metricsService: MetricsService,
@InjectRepository(ConnectedAccountEntity)
private readonly connectedAccountRepository: Repository<ConnectedAccountEntity>,
@InjectRepository(MessageChannelEntity)
@@ -43,6 +48,11 @@ export class GoogleMessagingNotificationHandler implements WebhookNotificationHa
async handle(request: GoogleMessagingNotificationRequest): Promise<void> {
await this.verify(request.authorizationHeader);
this.metricsService.incrementCounterBy({
key: MetricsKeys.ConnectedAccountSyncWebhookReceivedMessaging,
amount: 1,
});
const decodedData = this.decodeMessageData(request.body);
if (!isDefined(decodedData)) {
@@ -61,6 +71,10 @@ export class GoogleMessagingNotificationHandler implements WebhookNotificationHa
);
if (connectedAccountIds.length === 0) {
this.logger.warn(
'No Google connected account matches Gmail notification',
);
return;
}
@@ -71,12 +85,20 @@ export class GoogleMessagingNotificationHandler implements WebhookNotificationHa
},
});
if (messageChannels.length === 0) {
return;
}
for (const messageChannel of messageChannels) {
await this.webhookSyncTriggerService.triggerMessagingSync(
messageChannel.id,
messageChannel.workspaceId,
);
}
this.logger.log(
`Triggered messaging sync for ${messageChannels.length} message channels from Gmail notification`,
);
}
private async verify(authorizationHeader: string | undefined): Promise<void> {
@@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
@@ -12,6 +13,7 @@ import { GoogleMessagingNotificationHandler } from 'src/modules/connected-accoun
@Module({
imports: [
MetricsModule,
TwentyConfigModule,
WebhookSubscriptionModule,
TypeOrmModule.forFeature([
@@ -7,6 +7,8 @@ import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { In, Repository } from 'typeorm';
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
import { CalendarWebhookSubscriptionService } from 'src/modules/connected-account/webhook-subscription-manager/services/calendar-webhook-subscription.service';
import { WebhookSyncTriggerService } from 'src/modules/connected-account/webhook-subscription-manager/services/webhook-sync-trigger.service';
@@ -26,9 +28,17 @@ export class MicrosoftCalendarNotificationHandler implements WebhookNotification
private readonly calendarChannelRepository: Repository<CalendarChannelEntity>,
private readonly calendarWebhookSubscriptionService: CalendarWebhookSubscriptionService,
private readonly webhookSyncTriggerService: WebhookSyncTriggerService,
private readonly metricsService: MetricsService,
) {}
async handle(notifications: MicrosoftGraphNotification[]): Promise<void> {
if (notifications.length > 0) {
this.metricsService.incrementCounterBy({
key: MetricsKeys.ConnectedAccountSyncWebhookReceivedCalendar,
amount: notifications.length,
});
}
const subscriptionIds = notifications
.map((notification) => notification.subscriptionId)
.filter(isNonEmptyString);
@@ -96,6 +106,10 @@ export class MicrosoftCalendarNotificationHandler implements WebhookNotification
calendarChannel.id,
calendarChannel.workspaceId,
);
this.logger.log(
`Triggered calendar sync for calendar channel ${calendarChannel.id} from Microsoft notification`,
);
}
}
}
@@ -7,6 +7,8 @@ import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { In, Repository } from 'typeorm';
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { MessagingWebhookSubscriptionService } from 'src/modules/connected-account/webhook-subscription-manager/services/messaging-webhook-subscription.service';
import { WebhookSyncTriggerService } from 'src/modules/connected-account/webhook-subscription-manager/services/webhook-sync-trigger.service';
@@ -26,9 +28,17 @@ export class MicrosoftMessagingNotificationHandler implements WebhookNotificatio
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
private readonly messagingWebhookSubscriptionService: MessagingWebhookSubscriptionService,
private readonly webhookSyncTriggerService: WebhookSyncTriggerService,
private readonly metricsService: MetricsService,
) {}
async handle(notifications: MicrosoftGraphNotification[]): Promise<void> {
if (notifications.length > 0) {
this.metricsService.incrementCounterBy({
key: MetricsKeys.ConnectedAccountSyncWebhookReceivedMessaging,
amount: notifications.length,
});
}
const subscriptionIds = notifications
.map((notification) => notification.subscriptionId)
.filter(isNonEmptyString);
@@ -95,6 +105,10 @@ export class MicrosoftMessagingNotificationHandler implements WebhookNotificatio
messageChannel.id,
messageChannel.workspaceId,
);
this.logger.log(
`Triggered messaging sync for message channel ${messageChannel.id} from Microsoft notification`,
);
}
}
}
@@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
@@ -11,6 +12,7 @@ import { MicrosoftMessagingNotificationHandler } from 'src/modules/connected-acc
@Module({
imports: [
MetricsModule,
TwentyConfigModule,
WebhookSubscriptionModule,
TypeOrmModule.forFeature([MessageChannelEntity, CalendarChannelEntity]),