@@ -6,10 +6,12 @@ import * as os from 'os';
6
6
import * as path from 'path' ;
7
7
import * as util from 'util' ;
8
8
import * as zlib from 'zlib' ;
9
- import type { Envelope } from '@sentry/types' ;
9
+ import type { Envelope , EnvelopeItem } from '@sentry/types' ;
10
10
import { parseEnvelope } from '@sentry/utils' ;
11
11
12
+ const readFile = util . promisify ( fs . readFile ) ;
12
13
const writeFile = util . promisify ( fs . writeFile ) ;
14
+ const unlink = util . promisify ( fs . unlink ) ;
13
15
14
16
interface EventProxyServerOptions {
15
17
/** Port to start the event proxy server at. */
@@ -25,6 +27,99 @@ interface SentryRequestCallbackData {
25
27
sentryResponseStatusCode ?: number ;
26
28
}
27
29
30
+ const TEMPORARY_FILE_PATH = 'payload-files/temporary.json' ;
31
+
32
+ function isDateLikeString ( str : string ) : boolean {
33
+ // matches strings in the format "YYYY-MM-DD"
34
+ const datePattern = / ^ \d { 4 } - \d { 2 } - \d { 2 } / ;
35
+ return datePattern . test ( str ) ;
36
+ }
37
+
38
+ function extractPathFromUrl ( url : string ) : string {
39
+ const localhost = 'http://localhost:3030/' ;
40
+ return url . replace ( localhost , '' ) ;
41
+ }
42
+
43
+ function addCommaAfterEachLine ( data : string ) : string {
44
+ const jsonData = data . trim ( ) . split ( '\n' ) ;
45
+
46
+ const jsonDataWithCommas = jsonData . map ( ( item , index ) =>
47
+ index < jsonData . length - 1 ? item + ',' : item ,
48
+ ) ;
49
+
50
+ return jsonDataWithCommas . join ( '\n' ) ;
51
+ }
52
+
53
+ let idCounter = 1 ;
54
+ const idMap = new Map ( ) ;
55
+
56
+ function recursivelyReplaceData ( obj : any ) {
57
+ for ( let key in obj ) {
58
+ if ( typeof obj [ key ] === 'string' && isDateLikeString ( obj [ key ] ) ) {
59
+ obj [ key ] = `[[ISODateString]]` ;
60
+ } else if ( key . includes ( 'timestamp' ) ) {
61
+ obj [ key ] = `[[timestamp]]` ;
62
+ } else if ( typeof obj [ key ] === 'number' && obj [ key ] > 1000 ) {
63
+ obj [ key ] = `[[highNumber]]` ;
64
+ } else if ( key . includes ( '_id' ) ) {
65
+ if ( idMap . has ( obj [ key ] ) ) {
66
+ // give the same ID replacement to the same value
67
+ obj [ key ] = idMap . get ( obj [ key ] ) ;
68
+ } else {
69
+ const newId = `[[ID${ idCounter ++ } ]]` ;
70
+ idMap . set ( obj [ key ] , newId ) ;
71
+ obj [ key ] = newId ;
72
+ }
73
+ } else if ( typeof obj [ key ] === 'object' && obj [ key ] !== null ) {
74
+ recursivelyReplaceData ( obj [ key ] ) ;
75
+ }
76
+ }
77
+ }
78
+
79
+ function replaceDynamicValues ( data : string ) : string [ ] {
80
+ const jsonData = JSON . parse ( data ) ;
81
+
82
+ recursivelyReplaceData ( jsonData ) ;
83
+
84
+ // change remaining dynamic values
85
+ jsonData . forEach ( ( item : any ) => {
86
+ if ( item . trace ?. public_key ) {
87
+ item . trace . public_key = '[[publicKey]]' ;
88
+ }
89
+ } ) ;
90
+
91
+ return jsonData ;
92
+ }
93
+
94
+ /** This function transforms all dynamic data (like timestamps) from the temporarily saved file.
95
+ * The new content is saved into a new file with the url as the filename.
96
+ * The temporary file is deleted in the end.
97
+ */
98
+ async function transformSavedJSON ( ) {
99
+ try {
100
+ const data = await readFile ( TEMPORARY_FILE_PATH , 'utf8' ) ;
101
+
102
+ const jsonData = addCommaAfterEachLine ( data ) ;
103
+ const transformedJSON = replaceDynamicValues ( jsonData ) ;
104
+ const objWithReq = transformedJSON [ 2 ] as unknown as { request : { url : string } } ;
105
+
106
+ if ( 'request' in objWithReq ) {
107
+ const url = objWithReq . request . url ;
108
+ const filepath = `payload-files/${ extractPathFromUrl ( url ) } .json` ;
109
+
110
+ writeFile ( filepath , JSON . stringify ( transformedJSON , null , 2 ) ) . then ( ( ) => {
111
+ console . log ( `Successfully replaced data and saved file in ${ filepath } ` ) ;
112
+
113
+ unlink ( TEMPORARY_FILE_PATH ) . then ( ( ) =>
114
+ console . log ( `Successfully deleted ${ TEMPORARY_FILE_PATH } ` ) ,
115
+ ) ;
116
+ } ) ;
117
+ }
118
+ } catch ( err ) {
119
+ console . error ( 'Error' , err ) ;
120
+ }
121
+ }
122
+
28
123
/**
29
124
* Starts an event proxy server that will proxy events to sentry when the `tunnel` option is used. Point the `tunnel`
30
125
* option to this server (like this `tunnel: http://localhost:${port option}/`).
@@ -33,6 +128,8 @@ interface SentryRequestCallbackData {
33
128
export async function startEventProxyServer ( options : EventProxyServerOptions ) : Promise < void > {
34
129
const eventCallbackListeners : Set < ( data : string ) => void > = new Set ( ) ;
35
130
131
+ console . log ( `Proxy server "${ options . proxyServerName } " running. Waiting for events...` ) ;
132
+
36
133
const proxyServer = http . createServer ( ( proxyRequest , proxyResponse ) => {
37
134
const proxyRequestChunks : Uint8Array [ ] = [ ] ;
38
135
@@ -50,15 +147,24 @@ export async function startEventProxyServer(options: EventProxyServerOptions): P
50
147
? zlib . gunzipSync ( Buffer . concat ( proxyRequestChunks ) ) . toString ( )
51
148
: Buffer . concat ( proxyRequestChunks ) . toString ( ) ;
52
149
53
- let envelopeHeader = JSON . parse ( proxyRequestBody . split ( '\n' ) [ 0 ] ) ;
150
+ // save the JSON payload into a file
151
+ try {
152
+ writeFile ( TEMPORARY_FILE_PATH , `[${ proxyRequestBody } ]` ) . then ( ( ) => {
153
+ transformSavedJSON ( ) ;
154
+ } ) ;
155
+ } catch ( err ) {
156
+ console . error ( `Error writing file ${ TEMPORARY_FILE_PATH } ` , err ) ;
157
+ }
158
+
159
+ const envelopeHeader : EnvelopeItem [ 0 ] = JSON . parse ( proxyRequestBody . split ( '\n' ) [ 0 ] ) ;
54
160
55
161
if ( ! envelopeHeader . dsn ) {
56
162
throw new Error (
57
163
'[event-proxy-server] No dsn on envelope header. Please set tunnel option.' ,
58
164
) ;
59
165
}
60
166
61
- const { origin, pathname, host } = new URL ( envelopeHeader . dsn ) ;
167
+ const { origin, pathname, host } = new URL ( envelopeHeader . dsn as string ) ;
62
168
63
169
const projectId = pathname . substring ( 1 ) ;
64
170
const sentryIngestUrl = `${ origin } /api/${ projectId } /envelope/` ;
0 commit comments