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.

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)

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.