Skip to content
Open
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
91 changes: 91 additions & 0 deletions app/api/sentry-tunnel/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { NextRequest, NextResponse } from "next/server";
import { sentryConfig } from "@/app/lib/config";

export async function POST(request: NextRequest) {
try {
const body = await request.text();

if (!body) {
return NextResponse.json(
{ success: false, error: "No body provided" },
{ status: 400 }
);
}

// Parse the event
let event;
try {
event = JSON.parse(body);
} catch (e) {
return NextResponse.json(
{ success: false, error: "Invalid JSON" },
{ status: 400 }
);
}

// Check if Sentry is configured
if (!sentryConfig.enabled || !sentryConfig.serverUrl || !sentryConfig.projectId || !sentryConfig.publicKey) {
return NextResponse.json(
{ success: false, error: "Error tracking service not configured" },
{ status: 500 }
);
}

// Validate public key length (should be 32 characters for Sentry)
if (sentryConfig.publicKey.length !== 32) {
console.error("[Sentry Tunnel] Invalid public key length");
return NextResponse.json(
{ success: false, error: "Invalid Sentry configuration: public key length incorrect" },
{ status: 500 }
);
}

const storeUrl = `${sentryConfig.serverUrl}/api/${sentryConfig.projectId}/store/`;

// Sentry expects a 32-character hex string (UUID without dashes) in lowercase
const eventId = crypto.randomUUID().replace(/-/g, "").toLowerCase();

const eventWithId = {
event_id: eventId,
...event,
};

// Forward to Sentry with proper auth header
const authHeader = `Sentry sentry_version=7, sentry_key=${sentryConfig.publicKey}, sentry_client=sentry-api/1.0.0`;
const response = await fetch(storeUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Sentry-Auth": authHeader,
Accept: "*/*",
},
body: JSON.stringify(eventWithId),
});

const responseText = await response.text();

if (!response.ok) {
console.error(`[Sentry Tunnel] Error ${response.status}: ${responseText}`);
return NextResponse.json(
{
success: false,
error: "Failed to forward to Sentry",
},
{ status: 500 }
);
}

return NextResponse.json({ success: true, response: responseText });
} catch (error) {
console.error("Sentry tunnel error:", error);
return NextResponse.json(
{
success: false,
error:
error instanceof Error ? error.message : "Unknown error occurred",
},
{ status: 500 }
);
}
}

74 changes: 45 additions & 29 deletions app/hooks/analytics/analytics-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,36 +10,36 @@ export const ANALYTICS_EVENTS = {
// Page Events
PAGE_VIEW: 'Page Viewed',
PAGE_LOAD: 'Page Loaded',

// User Events
USER_LOGIN: 'User Login',
USER_LOGOUT: 'User Logout',
USER_REGISTER: 'User Register',
USER_IDENTIFY: 'User Identified',

// Transaction Events
TRANSACTION_STARTED: 'Transaction Started',
TRANSACTION_COMPLETED: 'Transaction Completed',
TRANSACTION_FAILED: 'Transaction Failed',
TRANSACTION_CANCELLED: 'Transaction Cancelled',

// UI Events
BUTTON_CLICK: 'Button Clicked',
LINK_CLICK: 'Link Clicked',
FORM_SUBMIT: 'Form Submitted',
MODAL_OPEN: 'Modal Opened',
MODAL_CLOSE: 'Modal Closed',

// Blog Events
BLOG_READ_START: 'Blog Reading Started',
BLOG_READ_COMPLETE: 'Blog Reading Completed',
BLOG_CARD_CLICK: 'Blog Card Clicked',
BLOG_SEARCH: 'Blog Search',

// Error Events
ERROR_OCCURRED: 'Error Occurred',
API_ERROR: 'API Error',

// Business Events
ORDER_CREATED: 'Order Created',
ORDER_UPDATED: 'Order Updated',
Expand All @@ -54,30 +54,30 @@ export const ANALYTICS_PROPERTIES = {
WALLET_ADDRESS: 'wallet_address',
LOGIN_METHOD: 'login_method',
IS_NEW_USER: 'is_new_user',

// Page Properties
PAGE_NAME: 'page_name',
PAGE_URL: 'page_url',
REFERRER: 'referrer',

// Transaction Properties
TRANSACTION_ID: 'transaction_id',
TRANSACTION_AMOUNT: 'transaction_amount',
TRANSACTION_CURRENCY: 'transaction_currency',
TRANSACTION_STATUS: 'transaction_status',
TRANSACTION_TYPE: 'transaction_type',

// UI Properties
ELEMENT_ID: 'element_id',
ELEMENT_TYPE: 'element_type',
ELEMENT_TEXT: 'element_text',
ELEMENT_POSITION: 'element_position',

// Error Properties
ERROR_MESSAGE: 'error_message',
ERROR_CODE: 'error_code',
ERROR_STACK: 'error_stack',

// Performance Properties
LOAD_TIME: 'load_time_ms',
RESPONSE_TIME: 'response_time_ms',
Expand Down Expand Up @@ -145,18 +145,34 @@ export const trackUserInteraction = (
/**
* Track errors with enhanced context
*/
export const trackError = (
error: Error,
context: Record<string, any> = {},
appName: string = APP_NAMES.NOBLOCKS
) => {
const isProd = typeof process !== 'undefined' && process.env.NODE_ENV === 'production';
trackAnalyticsEvent(ANALYTICS_EVENTS.ERROR_OCCURRED, {
[ANALYTICS_PROPERTIES.ERROR_MESSAGE]: error.message,
[ANALYTICS_PROPERTIES.ERROR_CODE]: error.name,
...(isProd ? {} : { [ANALYTICS_PROPERTIES.ERROR_STACK]: error.stack }),
...context,
}, appName);
export const trackError = (
error: Error,
context: Record<string, any> = {},
appName: string = APP_NAMES.NOBLOCKS
) => {
const isProd = typeof process !== 'undefined' && process.env.NODE_ENV === 'production';
trackAnalyticsEvent(ANALYTICS_EVENTS.ERROR_OCCURRED, {
[ANALYTICS_PROPERTIES.ERROR_MESSAGE]: error.message,
[ANALYTICS_PROPERTIES.ERROR_CODE]: error.name,
...(isProd ? {} : { [ANALYTICS_PROPERTIES.ERROR_STACK]: error.stack }),
...context,
}, appName);

// Track to Sentry for client-side errors
if (typeof window !== "undefined") {
import("@/app/lib/sentry").then(({ captureException }) => {
captureException(error, {
level: "error",
tags: {
app: appName,
errorSource: "analyticsUtils",
},
extra: context,
}).catch(() => {
// Silently fail
});
});
}
};

/**
Expand All @@ -178,7 +194,7 @@ export const trackPerformance = (
// Helper functions
function getSessionId(): string {
if (typeof window === 'undefined') return 'server';

let sessionId = sessionStorage.getItem('analytics_session_id');
if (!sessionId) {
sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
Expand All @@ -189,10 +205,10 @@ function getSessionId(): string {

function getElementType(elementId: string): string {
if (typeof window === 'undefined') return 'unknown';

const element = document.getElementById(elementId);
if (!element) return 'unknown';

return element.tagName.toLowerCase();
}

Expand All @@ -203,19 +219,19 @@ export const performanceTracker = {
performance.mark(`${name}_start`);
}
},

end: (name: string) => {
if (typeof window !== 'undefined' && 'performance' in window) {
performance.mark(`${name}_end`);
performance.measure(name, `${name}_start`, `${name}_end`);

const measure = performance.getEntriesByName(name)[0];
if (measure) {
trackPerformance(name, measure.duration);
}
}
},

measure: (name: string, fn: () => void) => {
performanceTracker.start(name);
fn();
Expand Down
17 changes: 17 additions & 0 deletions app/hooks/useSentry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* React hook for Sentry error tracking
* Follows the same pattern as useMixpanel
*/

import { useEffect } from "react";
import { initErrorHandlers } from "@/app/lib/sentry";

export function useSentry() {
useEffect(() => {
const cleanup = initErrorHandlers();
return cleanup;
}, []);
}

export { captureException } from "@/app/lib/sentry";

36 changes: 36 additions & 0 deletions app/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,39 @@ export const serverConfig = {
apiVersion: "2024-01-01", // Pin to a stable date
useCdn: false, // Set to false for fresh data
};

// Parse Sentry DSN to extract components
function parseDSN(dsn: string): { publicKey: string; serverUrl: string; projectId: string } | null {
try {
const match = dsn.match(/^https:\/\/([^@]+)@([^\/]+)\/(\d+)$/);
if (match) {
return {
publicKey: match[1],
serverUrl: `https://${match[2]}`,
projectId: match[3],
};
}
} catch {
// Invalid DSN format
}
return null;
}

const sentryDsn = process.env.NEXT_PUBLIC_SENTRY_DSN || "";
const sentryUrl = process.env.NEXT_PUBLIC_SENTRY_URL || "";
const dsnParts = sentryDsn ? parseDSN(sentryDsn) : null;

// Validate DSN parsing
if (sentryDsn && !dsnParts) {
console.warn("[Sentry Config] Failed to parse DSN:", sentryDsn);
}

export const sentryConfig = {
serverUrl: dsnParts?.serverUrl || sentryUrl,
projectId: dsnParts?.projectId,
publicKey: dsnParts?.publicKey,
enabled: Boolean(dsnParts?.projectId && dsnParts?.publicKey),
sampleRate: 1.0,
environment: process.env.NODE_ENV || "development",
release: "2.0.0",
};
Loading