hcengineering/platform · warning · ApiError

err.code

err.code

Error message

err.message (500 fallback sends err object when message is empty)

What it means

The analytics-collector server's global error handler behaves identically to the ai-bot one: ApiError instances are sent with their code/message, everything else gets a 500 with {message: err.message} or the raw error object when the message is empty. A response containing the whole error object indicates a non-ApiError with an empty message escaped from a route handler.

Solutions

  1. Inspect the raw error object returned in the 500 response to find the root cause.
  2. Convert expected failures (validation, upstream errors) into ApiError with a proper code and message.
  3. Ensure all async handlers await promises so rejections carry messages.
  4. Add logging/middleware to normalize thrown non-Error values into Errors.

Example fix

// before
sendEventsToPosthog(events, req) // floating promise
// after
try {
  await sendEventsToPosthog(events, req)
} catch (e) {
  throw new ApiError(502, `posthog delivery failed: ${String(e)}`)
}
Defensive patterns

Strategy: try-catch

Validate before calling

function extractServerMessage(body: unknown): string {
  if (typeof body === 'object' && body !== null && 'message' in body && typeof (body as any).message === 'string') return (body as any).message
  return 'unstructured analytics-collector error: ' + JSON.stringify(body)
}

Type guard

function isApiErrorPayload(x: unknown): x is { code: number; message: string } {
  return typeof x === 'object' && x !== null && typeof (x as any).code === 'number' && typeof (x as any).message === 'string'
}

Try / catch

app.use((err: unknown, req: Request, res: Response, _next: NextFunction) => {
  if (err instanceof ApiError) {
    return res.status(err.code).json({ code: err.code, message: err.message })
  }
  console.log(err)
  const message = err instanceof Error && err.message.length > 0 ? err.message : 'internal error'
  return res.status(500).json({ message })
})

Prevention

When it happens

Trigger: Unhandled exceptions in analytics ingestion routes that are not ApiError and carry an empty/undefined message — failed event validation libraries throwing bare errors, message-less rejections from sendEventsToPosthog, or thrown non-Error values.

Common situations: Posthog/network calls failing with opaque errors; schema validation throwing message-less errors; bugs throwing plain objects; missing await causing rejected promises to hit the error middleware.

Related errors


AI-assisted analysis of hcengineering/platform@63e28dc964 (2026-08-29). Data as JSON: /api/errors/fe99a59edc53ed99. Report an issue: GitHub.

Appendix: source

Thrown at services/analytics-collector/pod-analytics-collector/src/server.ts:289

        if (evt.event === AnalyticEventType.Error) {
          // eslint-disable-next-line @typescript-eslint/naming-convention
          const { error_message, error_type, error_stack } = evt.properties ?? {}
          reportOTELError({ message: error_message ?? 'Unknown error', stack: error_stack, name: error_type ?? '' })
        } else {
          reportOTEL('info', evt.event, evt.timestamp, { ...evt.properties, distinct_id: evt.distinct_id })
        }
      }
    })
  )

  app.use((err: any, _req: any, res: any, _next: any) => {
    console.log(err)
    if (err instanceof ApiError) {
      res.status(err.code).send({ code: err.code, message: err.message })
      return
    }

    res.status(500).send(err.message?.length > 0 ? { message: err.message } : err)
  })

  return app
}

async function sendEventsToPosthog (events: AnalyticEvent[], req: Request): Promise<void> {
  const posthogEvents: Record<string, any>[] = []
  for (const evt of events) {
    posthogEvents.push(await preparePostHogEvent(evt, req))
  }

  const payload = {
    api_key: config.PostHogAPI,
    batch: posthogEvents.reverse()
  }

  const posthogPayloadSize = JSON.stringify(payload).length
  console.log(`Sending to PostHog: ${posthogEvents.length} events, ${posthogPayloadSize} bytes`)

View on GitHub (pinned to 63e28dc964)