@@ -41,6 +41,11 @@ import {
4141import i18n from './i18n' ;
4242import { toUpdateError , UpdateError , UpdateErrorCode } from './error' ;
4343import { dedupeEndpoints , executeEndpointFallback } from './endpoint' ;
44+ import {
45+ resolveServerEventHash ,
46+ resolveServerEventType ,
47+ truncateDetail ,
48+ } from './telemetry' ;
4449
4550/**
4651 * Receives every error the client reports, alongside the report event type.
@@ -130,6 +135,9 @@ export class Pushy {
130135 clientType : 'Pushy' | 'Cresc' = 'Pushy' ;
131136 lastChecking ?: number ;
132137 lastRespJson ?: Promise < CheckResult > ;
138+ // Endpoint that most recently served a successful checkUpdate; telemetry
139+ // reuses it instead of re-running the fallback race.
140+ private lastWorkingEndpoint ?: string ;
133141
134142 version = cInfo . rnu ;
135143 loggerPromise = ( ( ) => {
@@ -207,6 +215,8 @@ export class Pushy {
207215 data ?: Record < string , string | number > ;
208216 } ) => {
209217 log ( `${ type } ${ code ? `[${ code } ] ` : '' } ${ message } ` ) ;
218+ // Fire-and-forget server telemetry; must not wait for the logger below.
219+ this . reportToServer ( { type, message, code, data } ) ;
210220 if ( this . options . logger === noop ) {
211221 // Wait briefly for a logger to arrive via setOptions (e.g. the rollback
212222 // report fires in the constructor before the user configures one), but
@@ -242,6 +252,72 @@ export class Pushy {
242252 log ( 'logger error:' , e ?. message || e ) ;
243253 }
244254 } ;
255+ /**
256+ * Best-effort lifecycle event reporting to the update server (aggregate
257+ * counts + sampled failure details power the version health view and the
258+ * rollback safety net server-side). Single POST to the last known working
259+ * endpoint, no retry, no fallback race; any failure is swallowed — telemetry
260+ * must never affect the update flow. Opt out with disableTelemetry.
261+ */
262+ private reportToServer = ( {
263+ type,
264+ message = '' ,
265+ code,
266+ data = { } ,
267+ } : {
268+ type : EventType ;
269+ message ?: string ;
270+ code ?: UpdateErrorCode ;
271+ data ?: Record < string , string | number > ;
272+ } ) => {
273+ try {
274+ if ( __DEV__ || this . options . disableTelemetry ) {
275+ return ;
276+ }
277+ const serverType = resolveServerEventType ( type , code ) ;
278+ if ( ! serverType ) {
279+ return ;
280+ }
281+ const { appKey } = this . options ;
282+ const endpoint =
283+ this . lastWorkingEndpoint || this . options . server ?. main ?. [ 0 ] ;
284+ if ( ! appKey || ! endpoint ) {
285+ return ;
286+ }
287+ const hash = resolveServerEventHash ( { serverType, data, currentVersion } ) ;
288+ if ( ! hash ) {
289+ return ;
290+ }
291+ const send = ( payloadType : typeof serverType , detail ?: string ) =>
292+ fetchWithTimeout (
293+ `${ endpoint } /report/${ appKey } ` ,
294+ {
295+ method : 'POST' ,
296+ headers : { 'Content-Type' : 'application/json' } ,
297+ body : JSON . stringify ( {
298+ type : payloadType ,
299+ hash,
300+ packageVersion :
301+ this . options . overridePackageVersion || packageVersion ,
302+ cInfo,
303+ detail : truncateDetail ( detail ) ,
304+ } ) ,
305+ } ,
306+ DEFAULT_FETCH_TIMEOUT_MS ,
307+ ) . catch ( ( e : any ) => {
308+ log ( 'telemetry report failed:' , e ?. message || e ) ;
309+ } ) ;
310+ send ( serverType , message || undefined ) ;
311+ // A download that only succeeded after an incremental patch failed is
312+ // still a patch_fail signal server-side (diff quality), carried in
313+ // data.error alongside the downloadSuccess event.
314+ if ( serverType === 'download_success' && data . error ) {
315+ send ( 'patch_fail' , String ( data . error ) ) ;
316+ }
317+ } catch ( e : any ) {
318+ log ( 'telemetry error:' , e ?. message || e ) ;
319+ }
320+ } ;
245321 throwIfEnabled = ( e : Error ) => {
246322 if ( this . options . throwError ) {
247323 throw e ;
@@ -387,6 +463,7 @@ export class Pushy {
387463 } ) ;
388464
389465 log ( 'check endpoint success' , endpoint ) ;
466+ this . lastWorkingEndpoint = endpoint ;
390467 return value ;
391468 } ;
392469 assertDebug = ( matter : string ) => {
0 commit comments