mirror of
https://github.com/selfxyz/self.git
synced 2026-02-19 02:24:25 -05:00
* feat: add structured NFC logging * fix ci * Fix: add deps * logging fixes. use breadcrumbs * fix android build * update SeverityLevel * [SELF-705] feat: add proof event logging (#1057) * feat: add proof event logging * refactor: unify sentry event logging * fix types * fix mock * simplify * code rabbit feedback * fix tests --------- Co-authored-by: seshanthS <seshanth@protonmail.com>
273 lines
6.2 KiB
TypeScript
273 lines
6.2 KiB
TypeScript
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
|
|
|
import { SENTRY_DSN } from '@env';
|
|
import {
|
|
addBreadcrumb,
|
|
captureException as sentryCaptureException,
|
|
captureFeedback as sentryCaptureFeedback,
|
|
captureMessage as sentryCaptureMessage,
|
|
feedbackIntegration,
|
|
init as sentryInit,
|
|
withProfiler,
|
|
withScope,
|
|
} from '@sentry/react';
|
|
|
|
interface BaseContext {
|
|
sessionId: string;
|
|
userId?: string;
|
|
platform: 'ios' | 'android';
|
|
stage: string;
|
|
}
|
|
|
|
// Security: Whitelist of allowed tag keys to prevent XSS
|
|
const ALLOWED_TAG_KEYS = new Set([
|
|
'session_id',
|
|
'platform',
|
|
'stage',
|
|
'circuitType',
|
|
'currentState',
|
|
'scanType',
|
|
'error_code',
|
|
'proof_step',
|
|
'scan_result',
|
|
'verification_status',
|
|
'document_type',
|
|
]);
|
|
|
|
// Security: Sanitize tag values to prevent XSS
|
|
const sanitizeTagValue = (value: unknown): string => {
|
|
if (value == null) return '';
|
|
|
|
const stringValue = String(value);
|
|
|
|
// Truncate to safe length
|
|
const MAX_TAG_LENGTH = 200;
|
|
const truncated =
|
|
stringValue.length > MAX_TAG_LENGTH
|
|
? stringValue.substring(0, MAX_TAG_LENGTH) + '...'
|
|
: stringValue;
|
|
|
|
// Escape HTML characters and remove potentially dangerous characters
|
|
return (
|
|
truncated
|
|
.replace(/[<>&"']/g, char => {
|
|
switch (char) {
|
|
case '<':
|
|
return '<';
|
|
case '>':
|
|
return '>';
|
|
case '&':
|
|
return '&';
|
|
case '"':
|
|
return '"';
|
|
case "'":
|
|
return ''';
|
|
default:
|
|
return char;
|
|
}
|
|
})
|
|
// Remove control characters and non-printable characters
|
|
.replace(/[^\x20-\x7E]/g, '')
|
|
);
|
|
};
|
|
|
|
// Security: Sanitize tag key to prevent XSS
|
|
const sanitizeTagKey = (key: string): string | null => {
|
|
// Only allow whitelisted keys
|
|
if (!ALLOWED_TAG_KEYS.has(key)) {
|
|
return null;
|
|
}
|
|
|
|
// Additional validation: alphanumeric and underscores only
|
|
if (!/^[a-zA-Z0-9_]+$/.test(key)) {
|
|
return null;
|
|
}
|
|
|
|
return key;
|
|
};
|
|
|
|
export interface NFCScanContext extends BaseContext, Record<string, unknown> {
|
|
scanType: 'mrz' | 'can';
|
|
}
|
|
|
|
export interface ProofContext extends BaseContext, Record<string, unknown> {
|
|
circuitType: 'register' | 'dsc' | 'disclose' | null;
|
|
currentState: string;
|
|
}
|
|
|
|
export const captureException = (
|
|
error: Error,
|
|
context?: Record<string, unknown>,
|
|
) => {
|
|
if (isSentryDisabled) {
|
|
return;
|
|
}
|
|
sentryCaptureException(error, {
|
|
extra: context,
|
|
});
|
|
};
|
|
|
|
export const captureFeedback = (
|
|
feedback: string,
|
|
context?: Record<string, unknown>,
|
|
) => {
|
|
if (isSentryDisabled) {
|
|
return;
|
|
}
|
|
|
|
sentryCaptureFeedback(
|
|
{
|
|
message: feedback,
|
|
name: context?.name as string | undefined,
|
|
email: context?.email as string | undefined,
|
|
tags: {
|
|
category: (context?.category as string) || 'general',
|
|
source: (context?.source as string) || 'feedback_modal',
|
|
},
|
|
},
|
|
{
|
|
captureContext: {
|
|
tags: {
|
|
category: (context?.category as string) || 'general',
|
|
source: (context?.source as string) || 'feedback_modal',
|
|
},
|
|
},
|
|
},
|
|
);
|
|
};
|
|
|
|
export const captureMessage = (
|
|
message: string,
|
|
context?: Record<string, unknown>,
|
|
) => {
|
|
if (isSentryDisabled) {
|
|
return;
|
|
}
|
|
sentryCaptureMessage(message, {
|
|
extra: context,
|
|
});
|
|
};
|
|
|
|
export const initSentry = () => {
|
|
if (isSentryDisabled) {
|
|
return;
|
|
}
|
|
|
|
sentryInit({
|
|
dsn: SENTRY_DSN,
|
|
debug: false,
|
|
// Performance Monitoring
|
|
tracesSampleRate: 1.0,
|
|
// Session Replay
|
|
replaysSessionSampleRate: 0.1,
|
|
replaysOnErrorSampleRate: 1.0,
|
|
// Disable collection of PII data
|
|
beforeSend(event) {
|
|
// Remove PII data
|
|
if (event.user) {
|
|
event.user.ip_address = undefined;
|
|
event.user.id = undefined;
|
|
}
|
|
return event;
|
|
},
|
|
integrations: [
|
|
feedbackIntegration({
|
|
buttonOptions: {
|
|
styles: {
|
|
triggerButton: {
|
|
position: 'absolute',
|
|
top: 20,
|
|
right: 20,
|
|
bottom: undefined,
|
|
marginTop: 100,
|
|
},
|
|
},
|
|
},
|
|
enableTakeScreenshot: true,
|
|
namePlaceholder: 'Fullname',
|
|
emailPlaceholder: 'Email',
|
|
}),
|
|
],
|
|
});
|
|
};
|
|
|
|
export const isSentryDisabled = !SENTRY_DSN;
|
|
|
|
type LogLevel = 'info' | 'warn' | 'error';
|
|
type LogCategory = 'proof' | 'nfc';
|
|
|
|
export const logEvent = (
|
|
level: LogLevel,
|
|
category: LogCategory,
|
|
message: string,
|
|
context: BaseContext & Record<string, unknown>,
|
|
extra?: Record<string, unknown>,
|
|
) => {
|
|
if (isSentryDisabled) {
|
|
return;
|
|
}
|
|
|
|
const { sessionId, userId, platform, stage, ...rest } = context;
|
|
const data = {
|
|
session_id: sessionId,
|
|
user_id: userId,
|
|
platform,
|
|
stage,
|
|
...rest,
|
|
...extra,
|
|
};
|
|
|
|
if (level === 'error') {
|
|
withScope(scope => {
|
|
scope.setLevel('error');
|
|
scope.setTag('session_id', sessionId);
|
|
scope.setTag('platform', platform);
|
|
scope.setTag('stage', stage);
|
|
Object.entries(rest).forEach(([key, value]) => {
|
|
const sanitizedKey = sanitizeTagKey(key);
|
|
if (sanitizedKey) {
|
|
const sanitizedValue = sanitizeTagValue(value);
|
|
scope.setTag(sanitizedKey, sanitizedValue);
|
|
}
|
|
});
|
|
if (userId) {
|
|
scope.setUser({ id: userId });
|
|
}
|
|
if (extra) {
|
|
Object.entries(extra).forEach(([key, value]) => {
|
|
scope.setExtra(key, value);
|
|
});
|
|
}
|
|
sentryCaptureMessage(message);
|
|
});
|
|
} else {
|
|
addBreadcrumb({
|
|
message,
|
|
level: level === 'warn' ? 'warning' : 'info',
|
|
category,
|
|
data,
|
|
timestamp: Date.now() / 1000,
|
|
});
|
|
}
|
|
};
|
|
|
|
export const logNFCEvent = (
|
|
level: LogLevel,
|
|
message: string,
|
|
context: NFCScanContext,
|
|
extra?: Record<string, unknown>,
|
|
) => logEvent(level, 'nfc', message, context, extra);
|
|
|
|
export const logProofEvent = (
|
|
level: LogLevel,
|
|
message: string,
|
|
context: ProofContext,
|
|
extra?: Record<string, unknown>,
|
|
) => logEvent(level, 'proof', message, context, extra);
|
|
|
|
export const wrapWithSentry = (App: React.ComponentType) => {
|
|
return isSentryDisabled ? App : withProfiler(App);
|
|
};
|