Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions packages/twenty-apps/hacktoberfest-2025/fireflies/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions packages/twenty-apps/hacktoberfest-2025/fireflies/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fireflies",
"version": "0.2.1",
"version": "0.2.2",
"license": "MIT",
"engines": {
"node": "^24.5.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -108,53 +112,45 @@ 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 };
}

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));
}
}
Expand All @@ -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 => {
Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
}

Expand All @@ -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;
}

Expand All @@ -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;
}

Expand All @@ -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;
}

Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent';

export interface LoggerConfig {
logLevel: LogLevel;
isTestEnvironment: boolean;
}

const LOG_LEVELS: Record<LogLevel, number> = {
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);
};
Loading