@@ -70,6 +70,66 @@ describe('/api/ingest', () => {
7070 expect ( response . status ) . toBe ( 400 ) ;
7171 expect ( response . headers . get ( 'access-control-allow-origin' ) ) . toBe ( '*' ) ;
7272 } ) ;
73+
74+ it . each ( [
75+ { event : 'tplane:invented' , properties : { transport : 'custom' } } ,
76+ { event : 'tplane:postinstall' , properties : { } } ,
77+ { event : 'tplane:stream_started' , properties : { } } ,
78+ { event : 'tplane:stream_started' , properties : 'secret' } ,
79+ { event : 'tplane:stream_started' , properties : [ 'secret' ] } ,
80+ { event : 'tplane:stream_started' , properties : { transport : 1 } } ,
81+ { event : 'tplane:browser_chat_init' , properties : { } } ,
82+ { event : 'tplane:stream_ended' , properties : { transport : 'custom' , durationMs : - 1 } } ,
83+ ] ) ( 'rejects malformed public payloads without capturing or echoing input' , async ( payload ) => {
84+ const response = await POST ( new Request ( 'https://threadplane.ai/api/ingest' , {
85+ method : 'POST' , body : JSON . stringify ( { distinctId : 'test' , ...payload } ) ,
86+ } ) as never ) ;
87+ expect ( response . status ) . toBe ( 400 ) ;
88+ expect ( response . headers . get ( 'access-control-allow-origin' ) ) . toBe ( '*' ) ;
89+ expect ( await response . json ( ) ) . toEqual ( { error : 'Invalid event payload' } ) ;
90+ expect ( capture ) . not . toHaveBeenCalled ( ) ;
91+ } ) ;
92+
93+ it ( 'accepts canonical runtime events while excluding arbitrary fields and person profiles' , async ( ) => {
94+ const response = await POST ( new Request ( 'https://threadplane.ai/api/ingest' , {
95+ method : 'POST' , body : JSON . stringify ( {
96+ distinctId : 'browser:test' , event : 'tplane:stream_ended' ,
97+ properties : {
98+ transport : 'langgraph' , surface : 'canonical_demo' , durationMs : 120 ,
99+ '0' : 'private' , command : 'private' , body : 'private' , token : 'private' ,
100+ $set : { email : 'private' } , $ip : '1.2.3.4' , $process_person_profile : true ,
101+ } ,
102+ } ) ,
103+ } ) as never ) ;
104+ expect ( response . status ) . toBe ( 202 ) ;
105+ expect ( capture ) . toHaveBeenCalledWith ( {
106+ distinctId : 'browser:test' , event : 'tplane:stream_ended' ,
107+ properties : { transport : 'langgraph' , surface : 'canonical_demo' , durationMs : 120 , $ip : null , $process_person_profile : false } ,
108+ } ) ;
109+ } ) ;
110+
111+ it ( 'rejects an oversized streamed body even without content-length' , async ( ) => {
112+ const response = await POST ( new Request ( 'https://threadplane.ai/api/ingest' , {
113+ method : 'POST' , body : JSON . stringify ( { distinctId : 'test' , event : 'tplane:browser_provided' , properties : { body : 'x' . repeat ( 16_384 ) } } ) ,
114+ } ) as never ) ;
115+ expect ( response . status ) . toBe ( 413 ) ;
116+ expect ( response . headers . get ( 'access-control-allow-origin' ) ) . toBe ( '*' ) ;
117+ expect ( capture ) . not . toHaveBeenCalled ( ) ;
118+ } ) ;
119+
120+ it ( 'reports provider failure without logging the raw exception' , async ( ) => {
121+ const log = vi . spyOn ( console , 'error' ) . mockImplementation ( ( ) => undefined ) ;
122+ shutdown . mockRejectedValueOnce ( new Error ( 'private provider response' ) ) ;
123+ try {
124+ const response = await POST ( new Request ( 'https://threadplane.ai/api/ingest' , {
125+ method : 'POST' , body : JSON . stringify ( { distinctId : 'test' , event : 'tplane:browser_provided' , properties : { } } ) ,
126+ } ) as never ) ;
127+ expect ( response . status ) . toBe ( 502 ) ;
128+ expect ( log ) . toHaveBeenCalledWith ( '[telemetry-ingest] capture failed' ) ;
129+ } finally {
130+ log . mockRestore ( ) ;
131+ }
132+ } ) ;
73133} ) ;
74134
75135/**
0 commit comments