BIO.RE
Analytics

Track Event

Authenticated event tracking — emits a FunnelEvent row tied to the current user. Killable via admin config. Validates JSON property payload size. Writes to the main Postgres database, not the Analytics DB.

POST /api/v1/analytics/track — 🔑 Bearer · Rate limit: 60 req / minute

Records a named event (signup step, action, button click, …) attributed to the current user. This endpoint coexists with the public session-tracking endpoints — but track writes to the main database (FunnelEvent row in @biore/prisma), not the separate Analytics DB. Use it for funnel events that must be reliably joined to the user table; use POST /analytics/session + POST /analytics/pageview for traffic instrumentation.

Killable from admin config. The service reads analytics.tracking_enabled (string, default 'true'). When set to anything other than 'true', the endpoint returns 200 { tracked: true } without writing — clients see success but no row lands. Useful for kill-switching analytics during incidents without redeploying.

properties payload is size-capped. Server reads analytics.max_event_data_bytes (default 10000) and runs validateEventData(properties, max). Anything over the cap throws 400 analytics.event.invalid_data. Keep payloads small; for large analytics blobs use a dedicated endpoint or batch.

Two databases, one module. This endpoint writes to prisma.funnelEvent (main @biore/prisma Postgres). The session/pageview endpoints write to analyticsDb (@biore/prisma-analytics — a separate Postgres on the Analytics DB service). Funnel events are joinable to the User table; session events are not. Plan your queries accordingly.

Request

Body — TrackEventDto

FieldTypeRequiredValidationNotes
eventstringIsStringEvent name (e.g. signup_step_complete, bio_link_click). No enum — free-form.
sessionIdstringoptionalIsStringOptional join key — typically the analytics session id from POST /session so funnel events can be correlated with traffic state. Stored verbatim — server doesn't validate it exists.
deviceTypestringoptionalIsStringFree-form (mobile / desktop / tablet). Stored as-is on FunnelEvent.eventData? No — the column is dropped from persistence (only event, userId, sessionId, eventData are written). Pass it for API symmetry; it's ignored.
localestringoptionalIsStringSame as deviceType — accepted by the DTO but the service drops it from the write.
propertiesobjectoptionalIsObjectArbitrary JSON. Persisted as FunnelEvent.eventData. Size-capped (see warn callout).

Headers

HeaderRequiredNotes
Authorization: Bearer <accessToken>Global JwtAuthGuard. userId is forwarded from the JWT — clients cannot impersonate.
Content-Type: application/jsonStandard.

Response

200 OKApiResponseOf<TrackingResultDto>

{
  "success": true,
  "data": { "tracked": true }
}
FieldTypeNotes
trackedbooleanAlways true when the request reaches the service. Does not signal that the row landed — both the kill switch (analytics.tracking_enabled !== 'true') and a swallowed-DB-write path can produce tracked: true with no DB effect. Treat as a "request accepted" ack only.

The controller hard-codes @HttpCode(200) so you don't get the framework default 201.

Errors

HTTPCode / i18nKeyReason
400analytics.event.invalid_dataproperties failed validateEventData (size cap or invalid shape). Payload may include the limit.
400(DTO validation)event missing / non-string.
401(guard)Missing / invalid bearer token.
429THROTTLE_LIMIT_EXCEEDEDOver 60 req/min from this IP. retryAfter (seconds) included in the response.

Side effects

  1. Decode bearer (global JwtAuthGuard); userId = req.user.id.
  2. ThrottleGuard increments throttle:<ip>:POST:/api/v1/analytics/track in Redis cache (60-req / 60s window).
  3. Read kill switch — configService.getWithDefault('analytics.tracking_enabled', 'true'). If not 'true' → return { tracked: true } with no further work.
  4. Read size cap — configService.getNumberWithDefault('analytics.max_event_data_bytes', 10000). Run validateEventData(properties, max). Failure → throw 400.
  5. Build event row: { id: randomUUID(), userId, sessionId, eventType: event, eventData: properties ?? {} }.
  6. prisma.funnelEvent.create({ data })direct write to the main DB, no queue. (A historical Redis-queued path was removed because the module never imported RedisModule.)
  7. Increment analyticsEventTrackedTotal Prometheus counter.
  8. Log [analytics] Event tracked: <event> for user <userId> (debug level).
  9. Return { tracked: true } (controller wraps as { success: true, data: { tracked: true } }).

Code samples

curl -X POST https://api.bio.re/api/v1/analytics/track \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "event": "bio_link_click",
    "sessionId": "session-uuid-from-create",
    "properties": { "linkId": "abc-123", "position": 2 }
  }'
type TrackInput = {
  event: string;
  sessionId?: string;
  properties?: Record<string, unknown>;
};

async function track(accessToken: string, input: TrackInput): Promise<void> {
  const res = await fetch('https://api.bio.re/api/v1/analytics/track', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${accessToken}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(input),
  });
  if (!res.ok) {
    const json = await res.json().catch(() => null);
    throw Object.assign(new Error(json?.error?.message ?? 'Track failed'), {
      code: json?.error?.code,
    });
  }
  // Don't await success — fire-and-forget at the call site if you don't care.
}
import { useMutation } from '@tanstack/react-query';

export function useTrackEvent() {
  return useMutation({
    mutationFn: async (input: TrackInput) => {
      const res = await fetch('/api/v1/analytics/track', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(input),
      });
      if (!res.ok) {
        // Don't surface to the user — instrumentation failures shouldn't be UX errors.
        return;
      }
    },
    // Don't retry — duplicates are worse than missing one event.
    retry: false,
  });
}

Try it

POST
/api/v1/analytics/track
AuthorizationBearer <token>

In: header

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

application/json

application/json

curl -X POST "https://loading/api/v1/analytics/track" \  -H "Content-Type: application/json" \  -d '{    "event": "string"  }'
{
  "success": true,
  "data": {
    "tracked": true
  }
}
{
  "success": false,
  "error": {
    "code": "AUTH_UNAUTHORIZED",
    "message": "Invalid credentials",
    "i18nKey": "auth.login.invalid_credentials",
    "i18nVars": {
      "field": "email"
    },
    "details": [
      {
        "message": "email must be an email"
      }
    ],
    "correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }
}

Source

SourcePathLines
Controllerapps/api-core/src/modules/analytics/analytics.controller.ts52–59 (class), 68–78 (track)
DTO (request)apps/api-core/src/modules/analytics/dto/index.ts9–15 (TrackEventDto)
DTO (response)apps/api-core/src/modules/analytics/dto/analytics-client-response.dto.ts5–8 (TrackingResultDto)
Serviceapps/api-core/src/modules/analytics/analytics.service.ts23–46 (trackEvent — kill switch, size cap, direct DB write)
Validationapps/api-core/src/common/json-validators.tsvalidateEventData (size cap enforcement)
Config keys(admin-managed)analytics.tracking_enabled (string, kill switch), analytics.max_event_data_bytes (number, default 10000)
Prisma modelpackages/prisma/prisma/schema.prismaFunnelEvent lines 1681–1695 (main DB — NOT analytics DB)

On this page