From 583d490cd7998e449f42fa4c18690f33e0765466 Mon Sep 17 00:00:00 2001 From: Alex Galey Date: Wed, 5 Nov 2025 14:16:18 +0100 Subject: [PATCH] feat: [Fireflies] log cleanly (#15618) ## [0.2.2] - 2025-11-04 ### Added - **Enhanced logging system**: Introduced configurable `AppLogger` class with log level support (debug, info, warn, error, silent) - Environment-based log level configuration via `LOG_LEVEL` environment variable - Test environment detection to prevent log noise during testing - Context-aware logging with proper prefixes for better debugging - **Improved error handling**: Enhanced webhook signature verification with detailed debug logging - **Better debugging capabilities**: Added comprehensive logging throughout webhook processing pipeline ### Enhanced - **Webhook signature verification**: Improved signature validation with detailed logging for troubleshooting - **Error messages**: More descriptive error logging for failed operations and security violations - **Development experience**: Better debugging information for webhook processing and API interactions --- .../hacktoberfest-2025/fireflies/.env.example | 12 +- .../hacktoberfest-2025/fireflies/CHANGELOG.md | 16 +++ .../fireflies/application.config.ts | 8 +- .../hacktoberfest-2025/fireflies/package.json | 2 +- .../src/fireflies-api-client.ts | 78 ++++++------- .../receive-fireflies-notes/src/logger.ts | 95 ++++++++++++++++ .../src/twenty-crm-service.ts | 35 +++--- .../src/webhook-handler.ts | 105 ++++++++---------- .../fireflies/src/__tests__/setup.ts | 4 +- 9 files changed, 219 insertions(+), 136 deletions(-) create mode 100644 packages/twenty-apps/hacktoberfest-2025/fireflies/serverlessFunctions/receive-fireflies-notes/src/logger.ts diff --git a/packages/twenty-apps/hacktoberfest-2025/fireflies/.env.example b/packages/twenty-apps/hacktoberfest-2025/fireflies/.env.example index 64a7e93a19..05135f11e6 100644 --- a/packages/twenty-apps/hacktoberfest-2025/fireflies/.env.example +++ b/packages/twenty-apps/hacktoberfest-2025/fireflies/.env.example @@ -77,10 +77,14 @@ FIREFLIES_MAX_POLLS=5 # Debugging & Logging # ============================================================================= -# Enable debug logging (true/false) -# When enabled, detailed logs will be output to console -# Useful for troubleshooting webhook processing -DEBUG_LOGS=false +# Log level: silent, error, warn, info, debug (default: error) +# Controls verbosity of console output +# - silent: No console output +# - error: Only errors (production default) +# - warn: Warnings and errors +# - info: Info, warnings, and errors +# - debug: All logs including detailed debugging +LOG_LEVEL=error # ============================================================================= # Configuration Notes diff --git a/packages/twenty-apps/hacktoberfest-2025/fireflies/CHANGELOG.md b/packages/twenty-apps/hacktoberfest-2025/fireflies/CHANGELOG.md index 914920f1b8..57e5605a05 100644 --- a/packages/twenty-apps/hacktoberfest-2025/fireflies/CHANGELOG.md +++ b/packages/twenty-apps/hacktoberfest-2025/fireflies/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## [0.2.2] - 2025-11-04 + +### Added +- **Enhanced logging system**: Introduced configurable `AppLogger` class with log level support (debug, info, warn, error, silent) + - Environment-based log level configuration via `LOG_LEVEL` environment variable + - Test environment detection to prevent log noise during testing + - Context-aware logging with proper prefixes for better debugging +- **Improved error handling**: Enhanced webhook signature verification with detailed debug logging +- **Better debugging capabilities**: Added comprehensive logging throughout webhook processing pipeline + +### Enhanced +- **Webhook signature verification**: Improved signature validation with detailed logging for troubleshooting +- **Error messages**: More descriptive error logging for failed operations and security violations +- **Development experience**: Better debugging information for webhook processing and API interactions + + ## [0.2.1] - 2025-11-03 ### Added diff --git a/packages/twenty-apps/hacktoberfest-2025/fireflies/application.config.ts b/packages/twenty-apps/hacktoberfest-2025/fireflies/application.config.ts index b0fcefb902..d690541fcf 100644 --- a/packages/twenty-apps/hacktoberfest-2025/fireflies/application.config.ts +++ b/packages/twenty-apps/hacktoberfest-2025/fireflies/application.config.ts @@ -31,10 +31,10 @@ const config: ApplicationConfig = { description: 'Whether to auto-create contacts for unknown participants', value: 'true', }, - DEBUG_LOGS: { - universalIdentifier: '009510df-5125-4683-941b-cce94b113242', - description: 'Enable verbose logging for debugging (true/false)', - value: 'false', + LOG_LEVEL: { + universalIdentifier: '2b019cf1-d198-48dd-943e-110571aa541e', + description: 'Log level: silent, error, warn, info, debug (default: error)', + value: 'error', }, FIREFLIES_SUMMARY_STRATEGY: { universalIdentifier: '562b43d9-cd47-4ec1-ae16-5cc7ebc9729b', diff --git a/packages/twenty-apps/hacktoberfest-2025/fireflies/package.json b/packages/twenty-apps/hacktoberfest-2025/fireflies/package.json index 1da398cf14..72a8f79a0b 100644 --- a/packages/twenty-apps/hacktoberfest-2025/fireflies/package.json +++ b/packages/twenty-apps/hacktoberfest-2025/fireflies/package.json @@ -1,6 +1,6 @@ { "name": "fireflies", - "version": "0.2.1", + "version": "0.2.2", "license": "MIT", "engines": { "node": "^24.5.0", diff --git a/packages/twenty-apps/hacktoberfest-2025/fireflies/serverlessFunctions/receive-fireflies-notes/src/fireflies-api-client.ts b/packages/twenty-apps/hacktoberfest-2025/fireflies/serverlessFunctions/receive-fireflies-notes/src/fireflies-api-client.ts index ffec8cd47b..b219c2b0d5 100644 --- a/packages/twenty-apps/hacktoberfest-2025/fireflies/serverlessFunctions/receive-fireflies-notes/src/fireflies-api-client.ts +++ b/packages/twenty-apps/hacktoberfest-2025/fireflies/serverlessFunctions/receive-fireflies-notes/src/fireflies-api-client.ts @@ -1,10 +1,14 @@ +import { createLogger } from './logger'; import type { FirefliesMeetingData, FirefliesParticipant, SummaryFetchConfig } from './types'; +const logger = createLogger('fireflies-api'); + export class FirefliesApiClient { private apiKey: string; constructor(apiKey: string) { if (!apiKey) { + logger.critical('FIREFLIES_API_KEY is required but not provided - this is a critical configuration error'); throw new Error('FIREFLIES_API_KEY is required'); } this.apiKey = apiKey; @@ -108,26 +112,22 @@ export class FirefliesApiClient { ): Promise<{ data: FirefliesMeetingData; summaryReady: boolean }> { // immediate_only: single attempt, no retries if (config.strategy === 'immediate_only') { - // eslint-disable-next-line no-console - console.log(`[fireflies-api] fetching meeting ${meetingId} (strategy: immediate_only)`); + logger.debug(`fetching meeting ${meetingId} (strategy: immediate_only)`); const meetingData = await this.fetchMeetingData(meetingId, { timeout: 10000 }); const ready = this.isSummaryReady(meetingData); - // eslint-disable-next-line no-console - console.log(`[fireflies-api] summary ready: ${ready}`); + logger.debug(`summary ready: ${ready}`); return { data: meetingData, summaryReady: ready }; } // immediate_with_retry: retry with exponential backoff - // eslint-disable-next-line no-console - console.log(`[fireflies-api] fetching meeting ${meetingId} (strategy: immediate_with_retry, maxAttempts: ${config.retryAttempts})`); + logger.debug(`fetching meeting ${meetingId} (strategy: immediate_with_retry, maxAttempts: ${config.retryAttempts})`); for (let attempt = 1; attempt <= config.retryAttempts; attempt++) { try { const meetingData = await this.fetchMeetingData(meetingId, { timeout: 10000 }); const ready = this.isSummaryReady(meetingData); - // eslint-disable-next-line no-console - console.log(`[fireflies-api] attempt ${attempt}/${config.retryAttempts}: summary ready=${ready}`); + logger.debug(`attempt ${attempt}/${config.retryAttempts}: summary ready=${ready}`); if (ready) { return { data: meetingData, summaryReady: true }; @@ -135,26 +135,22 @@ export class FirefliesApiClient { if (attempt < config.retryAttempts) { const delayMs = config.retryDelay * attempt; - // eslint-disable-next-line no-console - console.log(`[fireflies-api] summary not ready, waiting ${delayMs}ms before retry ${attempt + 1}`); + logger.debug(`summary not ready, waiting ${delayMs}ms before retry ${attempt + 1}`); await new Promise(resolve => setTimeout(resolve, delayMs)); } else { - // eslint-disable-next-line no-console - console.log(`[fireflies-api] max retries reached, returning partial data`); + logger.debug(`max retries reached, returning partial data`); return { data: meetingData, summaryReady: false }; } } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); - // eslint-disable-next-line no-console - console.error(`[fireflies-api] attempt ${attempt}/${config.retryAttempts} failed: ${errorMsg}`); + logger.error(`attempt ${attempt}/${config.retryAttempts} failed: ${errorMsg}`); if (attempt === config.retryAttempts) { throw error; } const delayMs = config.retryDelay * attempt; - // eslint-disable-next-line no-console - console.log(`[fireflies-api] retrying in ${delayMs}ms...`); + logger.debug(`retrying in ${delayMs}ms...`); await new Promise(resolve => setTimeout(resolve, delayMs)); } } @@ -174,18 +170,12 @@ export class FirefliesApiClient { const participantsWithEmails: FirefliesParticipant[] = []; const participantsNameOnly: FirefliesParticipant[] = []; - // eslint-disable-next-line no-console - console.log('[fireflies-api] === PARTICIPANT EXTRACTION DEBUG ==='); - // eslint-disable-next-line no-console - console.log('[fireflies-api] participants field:', JSON.stringify(transcript.participants)); - // eslint-disable-next-line no-console - console.log('[fireflies-api] meeting_attendees field:', JSON.stringify(transcript.meeting_attendees)); - // eslint-disable-next-line no-console - console.log('[fireflies-api] speakers field:', transcript.speakers?.map((s: any) => s.name)); - // eslint-disable-next-line no-console - console.log('[fireflies-api] meeting_attendance field:', transcript.meeting_attendance?.map((a: any) => a.name)); - // eslint-disable-next-line no-console - console.log('[fireflies-api] organizer_email:', transcript.organizer_email); + logger.debug('=== PARTICIPANT EXTRACTION DEBUG ==='); + logger.debug('participants field:', JSON.stringify(transcript.participants)); + logger.debug('meeting_attendees field:', JSON.stringify(transcript.meeting_attendees)); + logger.debug('speakers field:', transcript.speakers?.map((s: any) => s.name)); + logger.debug('meeting_attendance field:', transcript.meeting_attendance?.map((a: any) => a.name)); + logger.debug('organizer_email:', transcript.organizer_email); // Helper function to check if a string is an email const isEmail = (str: string): boolean => { @@ -214,12 +204,14 @@ export class FirefliesApiClient { parts.forEach(part => { const emailMatch = part.match(/<([^>]+)>/); const email = emailMatch ? emailMatch[1] : ''; - const name = part.replace(/[<>]/g, '').trim(); + // Extract name properly: if there's an email in angle brackets, get the part before it + const name = emailMatch + ? part.substring(0, part.indexOf('<')).trim() + : part.trim(); // Skip if the "name" is actually an email address if (isEmail(name)) { - // eslint-disable-next-line no-console - console.log(`[fireflies-api] Skipping participant with email as name: "${name}"`); + logger.debug(`Skipping participant with email as name: "${name}"`); return; } @@ -230,8 +222,7 @@ export class FirefliesApiClient { // Skip duplicates if (isDuplicate(name, email)) { - // eslint-disable-next-line no-console - console.log(`[fireflies-api] Skipping duplicate participant: "${name}" <${email}>`); + logger.debug(`Skipping duplicate participant: "${name}" <${email}>`); return; } @@ -252,8 +243,7 @@ export class FirefliesApiClient { // Skip if name is actually an email if (isEmail(name)) { - // eslint-disable-next-line no-console - console.log(`[fireflies-api] Skipping attendee with email as name: "${name}"`); + logger.debug(`Skipping attendee with email as name: "${name}"`); return; } @@ -274,8 +264,7 @@ export class FirefliesApiClient { // Skip if name is actually an email if (isEmail(name)) { - // eslint-disable-next-line no-console - console.log(`[fireflies-api] Skipping speaker with email as name: "${name}"`); + logger.debug(`Skipping speaker with email as name: "${name}"`); return; } @@ -292,8 +281,7 @@ export class FirefliesApiClient { // Skip if name is actually an email or contains comma-separated emails if (isEmail(name) || name.includes(',')) { - // eslint-disable-next-line no-console - console.log(`[fireflies-api] Skipping attendance with email/list as name: "${name}"`); + logger.debug(`Skipping attendance with email/list as name: "${name}"`); return; } @@ -372,14 +360,10 @@ export class FirefliesApiClient { // Return participants with emails first, then name-only participants const allParticipants = [...participantsWithEmails, ...participantsNameOnly]; - // eslint-disable-next-line no-console - console.log('[fireflies-api] === EXTRACTED PARTICIPANTS ==='); - // eslint-disable-next-line no-console - console.log('[fireflies-api] With emails:', participantsWithEmails.length, JSON.stringify(participantsWithEmails)); - // eslint-disable-next-line no-console - console.log('[fireflies-api] Name only:', participantsNameOnly.length, JSON.stringify(participantsNameOnly)); - // eslint-disable-next-line no-console - console.log('[fireflies-api] Total:', allParticipants.length); + logger.debug('=== EXTRACTED PARTICIPANTS ==='); + logger.debug('With emails:', participantsWithEmails.length, JSON.stringify(participantsWithEmails)); + logger.debug('Name only:', participantsNameOnly.length, JSON.stringify(participantsNameOnly)); + logger.debug('Total:', allParticipants.length); return allParticipants; } diff --git a/packages/twenty-apps/hacktoberfest-2025/fireflies/serverlessFunctions/receive-fireflies-notes/src/logger.ts b/packages/twenty-apps/hacktoberfest-2025/fireflies/serverlessFunctions/receive-fireflies-notes/src/logger.ts new file mode 100644 index 0000000000..758740574e --- /dev/null +++ b/packages/twenty-apps/hacktoberfest-2025/fireflies/serverlessFunctions/receive-fireflies-notes/src/logger.ts @@ -0,0 +1,95 @@ +export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent'; + +export interface LoggerConfig { + logLevel: LogLevel; + isTestEnvironment: boolean; +} + +const LOG_LEVELS: Record = { + debug: 0, + info: 1, + warn: 2, + error: 3, + silent: 4, +}; + +/** + * App-level Fireflies application logger with configurable log levels. + */ +export class AppLogger { + private config: LoggerConfig; + private context: string; + + constructor(context: string) { + this.context = context; + this.config = { + logLevel: this.parseLogLevel(process.env.LOG_LEVEL || 'error'), + isTestEnvironment: process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined, + }; + } + + private parseLogLevel(level: string): LogLevel { + const normalizedLevel = level.toLowerCase() as LogLevel; + return Object.keys(LOG_LEVELS).includes(normalizedLevel) ? normalizedLevel : 'error'; + } + + private shouldLog(level: LogLevel): boolean { + return LOG_LEVELS[level] >= LOG_LEVELS[this.config.logLevel]; + } + + /** + * Log debug information (LOG_LEVEL=debug) + */ + debug(message: string, ...args: any[]): void { + if (this.shouldLog('debug')) { + // eslint-disable-next-line no-console + console.log(`[${this.context}] ${message}`, ...args); + } + } + + /** + * Log informational messages (LOG_LEVEL=info or lower) + */ + info(message: string, ...args: any[]): void { + if (this.shouldLog('info')) { + // eslint-disable-next-line no-console + console.log(`[${this.context}] ${message}`, ...args); + } + } + + /** + * Log warnings (LOG_LEVEL=warn or lower) + */ + warn(message: string, ...args: any[]): void { + if (this.shouldLog('warn')) { + // eslint-disable-next-line no-console + console.warn(`[${this.context}] ${message}`, ...args); + } + } + + /** + * Log errors (LOG_LEVEL=error or lower) + */ + error(message: string, ...args: any[]): void { + if (this.shouldLog('error')) { + // eslint-disable-next-line no-console + console.error(`[${this.context}] ${message}`, ...args); + } + } + + /** + * Log critical errors that should ALWAYS be visible regardless of log level + * Use sparingly - only for fatal errors, security issues, or data corruption + */ + critical(message: string, ...args: any[]): void { + // eslint-disable-next-line no-console + console.error(`[${this.context}] CRITICAL: ${message}`, ...args); + } +} + +/** + * Factory function to create loggers with automatic context detection + */ +export const createLogger = (context: string): AppLogger => { + return new AppLogger(context); +}; diff --git a/packages/twenty-apps/hacktoberfest-2025/fireflies/serverlessFunctions/receive-fireflies-notes/src/twenty-crm-service.ts b/packages/twenty-apps/hacktoberfest-2025/fireflies/serverlessFunctions/receive-fireflies-notes/src/twenty-crm-service.ts index 9e85b7aea9..051ca4573c 100644 --- a/packages/twenty-apps/hacktoberfest-2025/fireflies/serverlessFunctions/receive-fireflies-notes/src/twenty-crm-service.ts +++ b/packages/twenty-apps/hacktoberfest-2025/fireflies/serverlessFunctions/receive-fireflies-notes/src/twenty-crm-service.ts @@ -1,3 +1,4 @@ +import { createLogger } from './logger'; import type { Contact, CreateMeetingResponse, @@ -11,6 +12,8 @@ import type { MeetingCreateInput, } from './types'; +const logger = createLogger('20 CRM Service'); + export class TwentyCrmService { private apiKey: string; private apiUrl: string; @@ -18,6 +21,7 @@ export class TwentyCrmService { constructor(apiKey: string, apiUrl: string) { if (!apiKey) { + logger.critical('TWENTY_API_KEY is required but not provided - this is a critical configuration error'); throw new Error('TWENTY_API_KEY is required'); } this.apiKey = apiKey; @@ -225,13 +229,13 @@ export class TwentyCrmService { const participantsWithEmails = participants.filter(p => p.email && p.email.trim()); const participantsNameOnly = participants.filter(p => !p.email || !p.email.trim()); - // Process participants with emails (original logic) + // Process participants with emails if (participantsWithEmails.length > 0) { const emailContactIds = await this.createContactsWithEmails(participantsWithEmails); newContactIds.push(...emailContactIds); } - // Process participants with names only (new logic) + // Process participants with names only if (participantsNameOnly.length > 0) { const nameContactIds = await this.createContactsNameOnly(participantsNameOnly); newContactIds.push(...nameContactIds); @@ -249,8 +253,7 @@ export class TwentyCrmService { if (!existing) { unique.push(participant); } else { - // eslint-disable-next-line no-console - console.warn(`[fireflies] Duplicate participant email detected: ${participant.email}. Using first occurrence.`); + logger.warn(`Duplicate participant email detected: ${participant.email}. Using first occurrence.`); } return unique; }, []); @@ -281,8 +284,7 @@ export class TwentyCrmService { } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; if (errorMessage.includes('Duplicate Emails') || errorMessage.includes('BAD_USER_INPUT')) { - // eslint-disable-next-line no-console - console.warn(`[fireflies] Skipping contact creation for ${participant.email} due to duplicate email constraint: ${errorMessage}`); + logger.warn(`Skipping contact creation for ${participant.email} due to duplicate email constraint: ${errorMessage}`); continue; } throw error; @@ -303,8 +305,7 @@ export class TwentyCrmService { if (!existing) { unique.push(participant); } else { - // eslint-disable-next-line no-console - console.warn(`[fireflies] Duplicate participant name detected: ${participant.name}. Using first occurrence.`); + logger.warn(`Duplicate participant name detected: ${participant.name}. Using first occurrence.`); } return unique; }, []); @@ -313,8 +314,7 @@ export class TwentyCrmService { // Check if we already have a contact with this exact name to avoid duplicates const existingContact = await this.findContactByName(participant.name); if (existingContact) { - // eslint-disable-next-line no-console - console.warn(`[fireflies] Contact with name "${participant.name}" already exists. Skipping creation.`); + logger.warn(`Contact with name "${participant.name}" already exists. Skipping creation.`); continue; } @@ -342,12 +342,10 @@ export class TwentyCrmService { } newContactIds.push(response.data.createPerson.id); - // eslint-disable-next-line no-console - console.log(`[fireflies] Created contact for name-only participant: ${participant.name}`); + logger.debug(`Created contact for name-only participant: ${participant.name}`); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - // eslint-disable-next-line no-console - console.warn(`[fireflies] Failed to create contact for ${participant.name}: ${errorMessage}`); + logger.warn(`Failed to create contact for ${participant.name}: ${errorMessage}`); // Continue processing other participants instead of failing completely continue; } @@ -446,8 +444,7 @@ export class TwentyCrmService { // Debug: log the variables being sent if (!this.isTestEnvironment) { - // eslint-disable-next-line no-console - console.log('[fireflies] createMeeting variables:', JSON.stringify(variables, null, 2)); + logger.debug('createMeeting variables:', JSON.stringify(variables, null, 2)); } const response = await this.gqlRequest(mutation, variables); @@ -503,8 +500,7 @@ export class TwentyCrmService { return json; } catch (error) { - // eslint-disable-next-line no-console - console.error('[twenty-crm] GraphQL request error:', error); + logger.error('GraphQL request error:', error); throw error; } } @@ -519,8 +515,7 @@ export class TwentyCrmService { const variables = { data: meetingData }; if (!this.isTestEnvironment) { - // eslint-disable-next-line no-console - console.log('[fireflies] createFailedMeeting variables:', JSON.stringify(variables, null, 2)); + logger.debug('createFailedMeeting variables:', JSON.stringify(variables, null, 2)); } const response = await this.gqlRequest(mutation, variables); diff --git a/packages/twenty-apps/hacktoberfest-2025/fireflies/serverlessFunctions/receive-fireflies-notes/src/webhook-handler.ts b/packages/twenty-apps/hacktoberfest-2025/fireflies/serverlessFunctions/receive-fireflies-notes/src/webhook-handler.ts index 886d759b5c..901d1bc5ce 100644 --- a/packages/twenty-apps/hacktoberfest-2025/fireflies/serverlessFunctions/receive-fireflies-notes/src/webhook-handler.ts +++ b/packages/twenty-apps/hacktoberfest-2025/fireflies/serverlessFunctions/receive-fireflies-notes/src/webhook-handler.ts @@ -1,6 +1,6 @@ -/* eslint-disable no-console */ import { FirefliesApiClient } from './fireflies-api-client'; import { MeetingFormatter } from './formatters'; +import { createLogger } from './logger'; import { TwentyCrmService } from './twenty-crm-service'; import type { FirefliesWebhookPayload, ProcessResult } from './types'; import { getApiUrl, getSummaryFetchConfig, shouldAutoCreateContacts } from './utils'; @@ -12,6 +12,8 @@ import { declare const process: { env: Record }; +const logger = createLogger('fireflies'); + export class WebhookHandler { private debug: string[] = []; private isTestEnvironment: boolean; @@ -29,19 +31,19 @@ export class WebhookHandler { }; try { - this.logDebug('[fireflies] invoked'); - this.logDebug(`[fireflies] apiUrl=${getApiUrl()}`); + logger.debug('invoked'); + logger.debug(`apiUrl=${getApiUrl()}`); // 0) Validate environment configuration const firefliesApiKey = process.env.FIREFLIES_API_KEY || ''; const twentyApiKey = process.env.TWENTY_API_KEY || ''; if (!firefliesApiKey) { - this.logError('[fireflies] FIREFLIES_API_KEY not configured'); + logger.critical('FIREFLIES_API_KEY not configured - this is a critical configuration error'); throw new Error('FIREFLIES_API_KEY environment variable is required'); } if (!twentyApiKey) { - this.logError('[fireflies] TWENTY_API_KEY not configured'); + logger.critical('TWENTY_API_KEY not configured - this is a critical configuration error'); throw new Error('TWENTY_API_KEY environment variable is required'); } @@ -49,20 +51,20 @@ export class WebhookHandler { const { payload, extractedHeaders } = this.parsePayload(params); const finalHeaders = extractedHeaders || headers; - this.logDebug(`[fireflies] payload meetingId=${payload.meetingId} eventType="${payload.eventType}"`); + logger.debug(`payload meetingId=${payload.meetingId} eventType="${payload.eventType}"`); // 2) Verify webhook signature const webhookSecret = process.env.FIREFLIES_WEBHOOK_SECRET || ''; const secretFingerprint = getWebhookSecretFingerprint(webhookSecret); - this.logDebug(`[fireflies] webhook secret fingerprint=${secretFingerprint}`); + logger.debug(`webhook secret fingerprint=${secretFingerprint}`); this.verifySignature(payload, finalHeaders, webhookSecret); - this.logDebug('[fireflies] signature verification: ok'); + logger.debug('signature verification: ok'); // 3) Fetch meeting data from Fireflies const summaryConfig = getSummaryFetchConfig(); - this.logDebug(`[fireflies] summary strategy: ${summaryConfig.strategy} (retryAttempts=${summaryConfig.retryAttempts}, retryDelay=${summaryConfig.retryDelay}ms)`); - this.logDebug(`[fireflies] fetching meeting data from Fireflies API`); + logger.debug(`summary strategy: ${summaryConfig.strategy} (retryAttempts=${summaryConfig.retryAttempts}, retryDelay=${summaryConfig.retryDelay}ms)`); + logger.debug(`fetching meeting data from Fireflies API`); const firefliesClient = new FirefliesApiClient(firefliesApiKey); const { data: meetingData, summaryReady } = await firefliesClient.fetchMeetingDataWithRetry( @@ -70,7 +72,7 @@ export class WebhookHandler { summaryConfig ); - this.logDebug(`[fireflies] meeting data fetched: title="${meetingData.title}" summaryReady=${summaryReady}`); + logger.debug(`meeting data fetched: title="${meetingData.title}" summaryReady=${summaryReady}`); result.summaryReady = summaryReady; result.summaryPending = !summaryReady; @@ -95,27 +97,27 @@ export class WebhookHandler { const existingMeeting = await twentyService.findExistingMeeting(meetingData.title); if (existingMeeting) { - this.logDebug(`[fireflies] meeting already exists id=${existingMeeting.id}`); + logger.debug(`meeting already exists id=${existingMeeting.id}`); result.success = true; result.meetingId = existingMeeting.id; result.debug = this.debug; return result; } - this.logDebug('[fireflies] no existing meeting found, proceeding'); + logger.debug('no existing meeting found, proceeding'); // 5) Match participants to existing contacts - this.logDebug(`[fireflies] total participants from API: ${meetingData.participants.length}`); + logger.debug(`total participants from API: ${meetingData.participants.length}`); meetingData.participants.forEach((p, idx) => { - this.logDebug(`[fireflies] participant ${idx + 1}: name="${p.name}" email="${p.email || 'none'}"`); + logger.debug(`participant ${idx + 1}: name="${p.name}" email="${p.email || 'none'}"`); }); const { matchedContacts, unmatchedParticipants } = await twentyService.matchParticipantsToContacts( meetingData.participants ); - this.logDebug(`[fireflies] matched=${matchedContacts.length} unmatched=${unmatchedParticipants.length}`); + logger.debug(`matched=${matchedContacts.length} unmatched=${unmatchedParticipants.length}`); unmatchedParticipants.forEach((p, idx) => { - this.logDebug(`[fireflies] unmatched ${idx + 1}: name="${p.name}" email="${p.email || 'none'}"`); + logger.debug(`unmatched ${idx + 1}: name="${p.name}" email="${p.email || 'none'}"`); }); // 6) Optionally create contacts @@ -124,7 +126,7 @@ export class WebhookHandler { ? await twentyService.createContactsForUnmatched(unmatchedParticipants) : []; result.newContacts = newContactIds; - this.logDebug(`[fireflies] autoCreate=${autoCreate} createdContacts=${newContactIds.length}`); + logger.debug(`autoCreate=${autoCreate} createdContacts=${newContactIds.length}`); // 7) Create note first (so we can link to it from the meeting) const allContactIds = [...matchedContacts.map(({ id }) => id), ...newContactIds]; @@ -134,13 +136,13 @@ export class WebhookHandler { noteBody ); result.noteIds = [noteId]; - this.logDebug(`[fireflies] created note id=${noteId}`); + logger.debug(`created note id=${noteId}`); // 8) Create meeting with direct relationship to the note const meetingInput = MeetingFormatter.toMeetingCreateInput(meetingData, noteId); - this.logDebug(`[fireflies] meeting duration: ${meetingData.duration} min (raw from API) → ${meetingInput.duration} min (rounded)`); + logger.debug(`meeting duration: ${meetingData.duration} min (raw from API) → ${meetingInput.duration} min (rounded)`); result.meetingId = await twentyService.createMeeting(meetingInput); - this.logDebug(`[fireflies] created meeting id=${result.meetingId} with noteId=${noteId}`); + logger.debug(`created meeting id=${result.meetingId} with noteId=${noteId}`); // 9) Link note to participants (Meeting link is handled via the relation field) await this.linkNoteToParticipants( @@ -148,12 +150,12 @@ export class WebhookHandler { noteId, allContactIds ); - this.logDebug(`[fireflies] linked note to ${allContactIds.length} participants`); + logger.debug(`linked note to ${allContactIds.length} participants`); result.success = true; } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; - this.logError(`[fireflies] error: ${message}`); + logger.error(`error: ${message}`); result.errors?.push(message); // Try to create a failed meeting record for tracking @@ -170,16 +172,16 @@ export class WebhookHandler { // Handle string-encoded params if (typeof normalizedParams === 'string') { - this.logDebug(`[fireflies] received params as string length=${normalizedParams.length}`); + logger.debug(`received params as string length=${normalizedParams.length}`); try { const parsed = JSON.parse(normalizedParams); normalizedParams = parsed; if (parsed && typeof parsed === 'object') { const parsedKeys = Object.keys(parsed as Record); - this.logDebug(`[fireflies] parsed params keys: ${parsedKeys.join(',') || 'none'}`); + logger.debug(`parsed params keys: ${parsedKeys.join(',') || 'none'}`); } } catch (parseError) { - this.logError(`[fireflies] error parsing string params: ${String(parseError)}`); + logger.error(`error parsing string params: ${String(parseError)}`); throw new Error('Invalid or missing webhook payload'); } } @@ -195,14 +197,14 @@ export class WebhookHandler { if (wrapper.headers && typeof wrapper.headers === 'object' && !Array.isArray(wrapper.headers)) { extractedHeaders = wrapper.headers as Record; const headerKeys = Object.keys(extractedHeaders); - this.logDebug(`[fireflies] extracted headers from wrapper: ${headerKeys.join(',')}`); + logger.debug(`extracted headers from wrapper: ${headerKeys.join(',')}`); } const wrapperKeys = ['params', 'payload', 'body', 'data', 'event']; for (const key of wrapperKeys) { const candidate = wrapper[key]; if (isValidFirefliesPayload(candidate)) { - this.logDebug(`[fireflies] detected payload under wrapper key "${key}"`); + logger.debug(`detected payload under wrapper key "${key}"`); payload = candidate as FirefliesWebhookPayload; break; } @@ -210,7 +212,7 @@ export class WebhookHandler { } if (!payload) { - this.logError('[fireflies] error: Invalid or missing webhook payload'); + logger.error('error: Invalid or missing webhook payload'); throw new Error('Invalid or missing webhook payload'); } @@ -218,7 +220,7 @@ export class WebhookHandler { const payloadRecord = payload as Record; const payloadKeys = Object.keys(payloadRecord); if (payloadKeys.length > 0) { - this.logDebug(`[fireflies] payload keys: ${payloadKeys.join(',')}`); + logger.debug(`payload keys: ${payloadKeys.join(',')}`); } return { payload, extractedHeaders }; @@ -233,7 +235,7 @@ export class WebhookHandler { const normalizedHeaders = headers || {}; const headerKeys = Object.keys(normalizedHeaders); if (headerKeys.length > 0) { - this.logDebug(`[fireflies] header keys: ${headerKeys.join(',')}`); + logger.debug(`header keys: ${headerKeys.join(',')}`); } const headerSignature = Object.entries(normalizedHeaders).find( @@ -247,7 +249,7 @@ export class WebhookHandler { : undefined; if (payloadSignature) { - this.logDebug('[fireflies] found signature inside payload'); + logger.debug('found signature inside payload'); } const signature = @@ -259,20 +261,20 @@ export class WebhookHandler { const signatureCheck = verifyWebhookSignature(body, signature, webhookSecret); if (!signatureCheck.isValid) { - this.logDebug( - `[fireflies] signature check failed. headerPresent=${Boolean( + logger.debug( + `signature check failed. headerPresent=${Boolean( headerSignature, )} payloadSignaturePresent=${Boolean(payloadSignature)}`, ); if (signature) { - this.logDebug(`[fireflies] provided signature=${signature}`); + logger.debug(`provided signature=${signature}`); } else { - this.logDebug('[fireflies] provided signature=undefined'); + logger.debug('provided signature=undefined'); } - this.logDebug( - `[fireflies] computed signature=${signatureCheck.computedSignature ?? 'unavailable'}`, + logger.debug( + `computed signature=${signatureCheck.computedSignature ?? 'unavailable'}`, ); - this.logError('[fireflies] error: Invalid webhook signature'); + logger.critical('Invalid webhook signature - potential security threat detected in production'); throw new Error('Invalid webhook signature'); } } @@ -286,34 +288,21 @@ export class WebhookHandler { for (const contactId of contactIds) { try { await twentyService.createNoteTarget(noteId, contactId); - this.logDebug(`[fireflies] linked note ${noteId} to person ${contactId}`); + logger.debug(`linked note ${noteId} to person ${contactId}`); } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; - this.logError(`[fireflies] failed to link note to person ${contactId}: ${message}`); + logger.error(`failed to link note to person ${contactId}: ${message}`); // Continue with other participants } } } - private logDebug(message: string): void { - this.debug.push(message); - if (!this.isTestEnvironment) { - console.log(message); - } - } - - private logError(message: string): void { - this.debug.push(message); - if (!this.isTestEnvironment) { - console.error(message); - } - } private async createFailedMeetingRecord(params: unknown, error: string): Promise { try { const twentyApiKey = process.env.TWENTY_API_KEY || ''; if (!twentyApiKey) { - this.logDebug('[fireflies] Cannot create failed meeting record: TWENTY_API_KEY not configured'); + logger.debug('Cannot create failed meeting record: TWENTY_API_KEY not configured'); return; } @@ -333,7 +322,7 @@ export class WebhookHandler { const meetingData = await firefliesClient.fetchMeetingData(meetingId); meetingTitle = meetingData.title || meetingTitle; } catch (fetchError) { - this.logDebug(`[fireflies] Could not fetch meeting title: ${fetchError instanceof Error ? fetchError.message : 'Unknown error'}`); + logger.debug(`Could not fetch meeting title: ${fetchError instanceof Error ? fetchError.message : 'Unknown error'}`); } } } @@ -346,10 +335,10 @@ export class WebhookHandler { ); const failedMeetingId = await twentyService.createFailedMeeting(failedMeetingData); - this.logDebug(`[fireflies] Created failed meeting record: ${failedMeetingId}`); + logger.debug(`Created failed meeting record: ${failedMeetingId}`); } catch (recordError) { // Don't throw here - we don't want to break the original error handling - this.logError(`[fireflies] Failed to create failed meeting record: ${recordError instanceof Error ? recordError.message : 'Unknown error'}`); + logger.error(`Failed to create failed meeting record: ${recordError instanceof Error ? recordError.message : 'Unknown error'}`); } } } diff --git a/packages/twenty-apps/hacktoberfest-2025/fireflies/src/__tests__/setup.ts b/packages/twenty-apps/hacktoberfest-2025/fireflies/src/__tests__/setup.ts index cda77d17fd..47f5b16e55 100644 --- a/packages/twenty-apps/hacktoberfest-2025/fireflies/src/__tests__/setup.ts +++ b/packages/twenty-apps/hacktoberfest-2025/fireflies/src/__tests__/setup.ts @@ -8,9 +8,9 @@ process.env.FIREFLIES_WEBHOOK_SECRET = 'testsecret'; process.env.AUTO_CREATE_CONTACTS = 'true'; process.env.SERVER_URL = 'http://localhost:3000'; process.env.TWENTY_API_KEY = 'test-api-key'; -process.env.DEBUG_LOGS = 'true'; // Enable debug logs in tests +process.env.LOG_LEVEL = 'silent'; // Reset mocks before each test beforeEach(() => { jest.clearAllMocks(); -}); \ No newline at end of file +});