1+ import { NextRequest , NextResponse } from 'next/server'
2+ import { createLogger } from '@/lib/logs/console-logger'
3+
4+ const logger = createLogger ( 'TelemetryAPI' )
5+
6+ const ALLOWED_CATEGORIES = [
7+ 'page_view' ,
8+ 'feature_usage' ,
9+ 'performance' ,
10+ 'error' ,
11+ 'workflow' ,
12+ 'consent' ,
13+ ]
14+
15+ const DEFAULT_TIMEOUT = 5000 // 5 seconds timeout
16+
17+ /**
18+ * Validates telemetry data to ensure it doesn't contain sensitive information
19+ */
20+ function validateTelemetryData ( data : any ) : boolean {
21+ if ( ! data || typeof data !== 'object' ) {
22+ return false
23+ }
24+
25+ if ( ! data . category || ! data . action ) {
26+ return false
27+ }
28+
29+ if ( ! ALLOWED_CATEGORIES . includes ( data . category ) ) {
30+ return false
31+ }
32+
33+ const jsonStr = JSON . stringify ( data ) . toLowerCase ( )
34+ const sensitivePatterns = [
35+ / p a s s w o r d / ,
36+ / t o k e n / ,
37+ / s e c r e t / ,
38+ / k e y / ,
39+ / a u t h / ,
40+ / c r e d e n t i a l / ,
41+ / p r i v a t e / ,
42+ ]
43+
44+ return ! sensitivePatterns . some ( pattern => pattern . test ( jsonStr ) )
45+ }
46+
47+ /**
48+ * Safely converts a value to string, handling undefined and null values
49+ */
50+ function safeStringValue ( value : any ) : string {
51+ if ( value === undefined || value === null ) {
52+ return ''
53+ }
54+
55+ try {
56+ return String ( value )
57+ } catch ( e ) {
58+ return ''
59+ }
60+ }
61+
62+ /**
63+ * Creates a safe attribute object for OpenTelemetry
64+ */
65+ function createSafeAttributes ( data : Record < string , any > ) : Array < { key : string , value : { stringValue : string } } > {
66+ if ( ! data || typeof data !== 'object' ) {
67+ return [ ]
68+ }
69+
70+ const attributes : Array < { key : string , value : { stringValue : string } } > = [ ]
71+
72+ Object . entries ( data ) . forEach ( ( [ key , value ] ) => {
73+ if ( value !== undefined && value !== null && key ) {
74+ attributes . push ( {
75+ key,
76+ value : { stringValue : safeStringValue ( value ) }
77+ } )
78+ }
79+ } )
80+
81+ return attributes
82+ }
83+
84+ /**
85+ * Forwards telemetry data to OpenTelemetry collector
86+ */
87+ async function forwardToCollector ( data : any ) : Promise < boolean > {
88+ if ( ! data || typeof data !== 'object' ) {
89+ logger . error ( 'Invalid telemetry data format' )
90+ return false
91+ }
92+
93+ const endpoint = process . env . TELEMETRY_ENDPOINT || 'https://telemetry.simstudio.ai/v1/traces'
94+ const timeout = parseInt ( process . env . TELEMETRY_TIMEOUT || '' ) || DEFAULT_TIMEOUT
95+
96+ try {
97+ const timestamp = Date . now ( ) * 1000000
98+
99+ const safeAttrs = createSafeAttributes ( data )
100+
101+ const serviceAttrs = [
102+ { key : 'service.name' , value : { stringValue : 'sim-studio' } } ,
103+ { key : 'service.version' , value : { stringValue : process . env . NEXT_PUBLIC_APP_VERSION || '0.1.0' } } ,
104+ { key : 'deployment.environment' , value : { stringValue : process . env . NODE_ENV || 'production' } }
105+ ]
106+
107+ const spanName = data . category && data . action ? `${ data . category } .${ data . action } ` : 'telemetry.event'
108+
109+ const payload = {
110+ resourceSpans : [ {
111+ resource : {
112+ attributes : serviceAttrs
113+ } ,
114+ instrumentationLibrarySpans : [ {
115+ spans : [ {
116+ name : spanName ,
117+ kind : 1 ,
118+ startTimeUnixNano : timestamp ,
119+ endTimeUnixNano : timestamp + 1000000 ,
120+ attributes : safeAttrs
121+ } ]
122+ } ]
123+ } ]
124+ }
125+
126+ // Safe debug log of the payload structure without sensitive data
127+ logger . debug ( 'Preparing to send telemetry payload' , {
128+ endpoint,
129+ hasAttributes : safeAttrs . length > 0 ,
130+ attributeCount : safeAttrs . length
131+ } )
132+
133+ // Create explicit AbortController for timeout
134+ const controller = new AbortController ( )
135+ const timeoutId = setTimeout ( ( ) => controller . abort ( ) , timeout )
136+
137+ try {
138+ const options = {
139+ method : 'POST' ,
140+ headers : {
141+ 'Content-Type' : 'application/json'
142+ } ,
143+ body : JSON . stringify ( payload ) ,
144+ signal : controller . signal
145+ }
146+
147+ const response = await fetch ( endpoint , options )
148+ clearTimeout ( timeoutId )
149+
150+ if ( ! response . ok ) {
151+ logger . error ( 'Telemetry collector returned error' , {
152+ status : response . status ,
153+ statusText : response . statusText
154+ } )
155+ return false
156+ }
157+
158+ return true
159+ } catch ( fetchError ) {
160+ clearTimeout ( timeoutId )
161+ if ( fetchError instanceof Error && fetchError . name === 'AbortError' ) {
162+ logger . error ( 'Telemetry request timed out' , { endpoint } )
163+ } else {
164+ logger . error ( 'Failed to send telemetry to collector' , fetchError )
165+ }
166+ return false
167+ }
168+ } catch ( error ) {
169+ logger . error ( 'Error preparing telemetry payload' , error )
170+ return false
171+ }
172+ }
173+
174+ /**
175+ * Endpoint that receives telemetry events and forwards them to OpenTelemetry collector
176+ */
177+ export async function POST ( req : NextRequest ) {
178+ try {
179+ let eventData
180+ try {
181+ eventData = await req . json ( )
182+ } catch ( parseError ) {
183+ return NextResponse . json (
184+ { error : 'Invalid JSON in request body' } ,
185+ { status : 400 }
186+ )
187+ }
188+
189+ if ( ! validateTelemetryData ( eventData ) ) {
190+ return NextResponse . json (
191+ { error : 'Invalid telemetry data format or contains sensitive information' } ,
192+ { status : 400 }
193+ )
194+ }
195+
196+ const forwarded = await forwardToCollector ( eventData )
197+
198+ return NextResponse . json ( {
199+ success : true ,
200+ forwarded
201+ } )
202+ } catch ( error ) {
203+ logger . error ( 'Error processing telemetry event' , error )
204+ return NextResponse . json (
205+ { error : 'Failed to process telemetry event' } ,
206+ { status : 500 }
207+ )
208+ }
209+ }
0 commit comments