- Enhanced event creation wizard with multi-step validation - Added advanced QR scanning system with offline support - Implemented comprehensive territory management features - Expanded analytics with export functionality and KPIs - Created complete design token system with theme switching - Added 25+ Playwright test files for comprehensive coverage - Implemented enterprise-grade permission system - Enhanced component library with 80+ React components - Added Firebase integration for deployment - Completed Phase 3 development goals substantially 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
310 lines
11 KiB
JavaScript
310 lines
11 KiB
JavaScript
"use strict";
|
|
/**
|
|
* Structured Logger Utility for Firebase Cloud Functions
|
|
*
|
|
* Provides consistent structured logging with proper data masking
|
|
* and performance tracking for scanner operations.
|
|
*/
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || (function () {
|
|
var ownKeys = function(o) {
|
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
var ar = [];
|
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
return ar;
|
|
};
|
|
return ownKeys(o);
|
|
};
|
|
return function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
})();
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.Sentry = exports.logger = void 0;
|
|
exports.withLogging = withLogging;
|
|
const firebase_functions_1 = require("firebase-functions");
|
|
const Sentry = __importStar(require("@sentry/node"));
|
|
exports.Sentry = Sentry;
|
|
// Initialize Sentry for Cloud Functions
|
|
const initializeSentry = () => {
|
|
// Only initialize if DSN is provided and not a mock
|
|
const dsn = process.env.SENTRY_DSN;
|
|
if (!dsn || dsn.includes('mock')) {
|
|
console.info('Sentry: Skipping initialization (no DSN or mock DSN detected)');
|
|
return;
|
|
}
|
|
Sentry.init({
|
|
dsn,
|
|
environment: process.env.NODE_ENV || 'production',
|
|
tracesSampleRate: 0.1,
|
|
integrations: [
|
|
// Add Node.js specific integrations
|
|
Sentry.httpIntegration(),
|
|
Sentry.expressIntegration(),
|
|
],
|
|
beforeSend: (event, hint) => {
|
|
// Filter out noisy errors
|
|
if (event.exception?.values?.[0]?.type === 'TypeError' &&
|
|
event.exception?.values?.[0]?.value?.includes('fetch')) {
|
|
return null;
|
|
}
|
|
return event;
|
|
},
|
|
});
|
|
};
|
|
// Initialize Sentry when module loads
|
|
initializeSentry();
|
|
/**
|
|
* Mask sensitive data in QR codes, tokens, or other sensitive strings
|
|
*/
|
|
function maskSensitiveData(data) {
|
|
if (!data || data.length <= 8) {
|
|
return '***';
|
|
}
|
|
// Show first 4 and last 4 characters, mask the middle
|
|
const start = data.substring(0, 4);
|
|
const end = data.substring(data.length - 4);
|
|
const maskLength = Math.min(data.length - 8, 20); // Cap mask length
|
|
const mask = '*'.repeat(maskLength);
|
|
return `${start}${mask}${end}`;
|
|
}
|
|
/**
|
|
* Format log context with sensitive data masking
|
|
*/
|
|
function formatLogContext(context) {
|
|
const formatted = {};
|
|
// Copy non-sensitive fields directly
|
|
const safeCopyFields = ['sessionId', 'accountId', 'orgId', 'eventId', 'ticketTypeId', 'deviceId', 'userId', 'operation'];
|
|
for (const field of safeCopyFields) {
|
|
if (context[field]) {
|
|
formatted[field] = context[field];
|
|
}
|
|
}
|
|
// Mask sensitive fields
|
|
if (context.qr) {
|
|
formatted.qr_masked = maskSensitiveData(context.qr);
|
|
}
|
|
if (context.deviceId) {
|
|
formatted.device_short = context.deviceId.split('_')[1]?.substring(0, 8) || 'unknown';
|
|
}
|
|
formatted.timestamp = new Date().toISOString();
|
|
return formatted;
|
|
}
|
|
/**
|
|
* Core structured logger class
|
|
*/
|
|
class StructuredLogger {
|
|
/**
|
|
* Log scanner verification result with full context
|
|
*/
|
|
logScannerVerify(data) {
|
|
const logData = {
|
|
...formatLogContext(data),
|
|
result: data.result,
|
|
latencyMs: data.latencyMs,
|
|
reason: data.reason,
|
|
timestamp: data.timestamp || new Date().toISOString(),
|
|
};
|
|
// Use different log levels based on result
|
|
if (data.result === 'valid') {
|
|
firebase_functions_1.logger.info('Scanner verification successful', logData);
|
|
}
|
|
else if (data.result === 'already_scanned') {
|
|
firebase_functions_1.logger.warn('Scanner verification - already scanned', logData);
|
|
}
|
|
else {
|
|
firebase_functions_1.logger.warn('Scanner verification failed', logData);
|
|
}
|
|
// Send to Sentry if it's an error or concerning result
|
|
if (data.result === 'invalid' && data.reason !== 'ticket_not_found') {
|
|
Sentry.withScope((scope) => {
|
|
scope.setTag('feature', 'scanner');
|
|
scope.setTag('scanner.result', data.result);
|
|
scope.setContext('scanner_verification', logData);
|
|
Sentry.captureMessage(`Scanner verification failed: ${data.reason}`, 'warning');
|
|
});
|
|
}
|
|
}
|
|
/**
|
|
* Log performance metrics for scanner operations
|
|
*/
|
|
logPerformance(data) {
|
|
const logData = {
|
|
operation: data.operation,
|
|
duration_ms: data.duration,
|
|
...(data.context ? formatLogContext(data.context) : {}),
|
|
metadata: data.metadata,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
firebase_functions_1.logger.info('Performance metric', logData);
|
|
// Send slow operations to Sentry
|
|
if (data.duration > 5000) { // Operations slower than 5 seconds
|
|
Sentry.withScope((scope) => {
|
|
scope.setTag('feature', 'performance');
|
|
scope.setTag('performance.operation', data.operation);
|
|
scope.setContext('performance_metric', logData);
|
|
Sentry.captureMessage(`Slow operation: ${data.operation} took ${data.duration}ms`, 'warning');
|
|
});
|
|
}
|
|
}
|
|
/**
|
|
* Log general information with context
|
|
*/
|
|
info(message, context, metadata) {
|
|
const logData = {
|
|
message,
|
|
...(context ? formatLogContext(context) : {}),
|
|
...metadata,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
firebase_functions_1.logger.info(message, logData);
|
|
}
|
|
/**
|
|
* Log warnings with context
|
|
*/
|
|
warn(message, context, metadata) {
|
|
const logData = {
|
|
message,
|
|
...(context ? formatLogContext(context) : {}),
|
|
...metadata,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
firebase_functions_1.logger.warn(message, logData);
|
|
// Send warnings to Sentry with context
|
|
Sentry.withScope((scope) => {
|
|
if (context?.operation) {
|
|
scope.setTag('operation', context.operation);
|
|
}
|
|
scope.setContext('warning_context', logData);
|
|
Sentry.captureMessage(message, 'warning');
|
|
});
|
|
}
|
|
/**
|
|
* Log errors with context and send to Sentry
|
|
*/
|
|
error(message, error, context, metadata) {
|
|
const logData = {
|
|
message,
|
|
error_message: error?.message,
|
|
error_stack: error?.stack,
|
|
...(context ? formatLogContext(context) : {}),
|
|
...metadata,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
firebase_functions_1.logger.error(message, logData);
|
|
// Send to Sentry with full context
|
|
Sentry.withScope((scope) => {
|
|
if (context?.operation) {
|
|
scope.setTag('operation', context.operation);
|
|
}
|
|
if (context?.sessionId) {
|
|
scope.setTag('scanner.session', context.sessionId);
|
|
}
|
|
scope.setContext('error_context', logData);
|
|
if (error) {
|
|
Sentry.captureException(error);
|
|
}
|
|
else {
|
|
Sentry.captureMessage(message, 'error');
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Log debug information (only in development)
|
|
*/
|
|
debug(message, context, metadata) {
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
const logData = {
|
|
message,
|
|
...(context ? formatLogContext(context) : {}),
|
|
...metadata,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
firebase_functions_1.logger.debug(message, logData);
|
|
}
|
|
}
|
|
/**
|
|
* Capture exception directly to Sentry with context
|
|
*/
|
|
captureException(error, context) {
|
|
Sentry.withScope((scope) => {
|
|
if (context) {
|
|
scope.setContext('exception_context', formatLogContext(context));
|
|
if (context.operation) {
|
|
scope.setTag('operation', context.operation);
|
|
}
|
|
if (context.sessionId) {
|
|
scope.setTag('scanner.session', context.sessionId);
|
|
}
|
|
}
|
|
Sentry.captureException(error);
|
|
});
|
|
}
|
|
/**
|
|
* Start a performance transaction
|
|
*/
|
|
startTransaction(name, op) {
|
|
return Sentry.startSpan({ name, op }, () => { });
|
|
}
|
|
/**
|
|
* Add breadcrumb for debugging
|
|
*/
|
|
addBreadcrumb(message, category = 'general', data) {
|
|
Sentry.addBreadcrumb({
|
|
message,
|
|
category,
|
|
level: 'info',
|
|
data: {
|
|
timestamp: new Date().toISOString(),
|
|
...data,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
// Singleton logger instance
|
|
exports.logger = new StructuredLogger();
|
|
/**
|
|
* Middleware wrapper for Cloud Functions to automatically log performance
|
|
*/
|
|
function withLogging(operationName, fn, contextExtractor) {
|
|
return async (...args) => {
|
|
const startTime = performance.now();
|
|
const context = contextExtractor ? contextExtractor(...args) : undefined;
|
|
exports.logger.addBreadcrumb(`Starting operation: ${operationName}`, 'function', context);
|
|
try {
|
|
const result = await fn(...args);
|
|
const duration = performance.now() - startTime;
|
|
exports.logger.logPerformance({
|
|
operation: operationName,
|
|
duration,
|
|
context,
|
|
});
|
|
return result;
|
|
}
|
|
catch (error) {
|
|
const duration = performance.now() - startTime;
|
|
exports.logger.error(`Operation failed: ${operationName}`, error, context, { duration });
|
|
throw error;
|
|
}
|
|
};
|
|
}
|
|
//# sourceMappingURL=logger.js.map
|