Skip to main content

Analytics

The analytics feature exposes a provider and hook utilities that let the search embed collect events across multiple analytics services (Mixpanel and PostHog) while keeping visitor identity in sync with the IndexedDB ORM. Everything is exported from @superbright/indexeddb-orm.

import { AnalyticsProvider, useAnalytics } from "@superbright/indexeddb-orm";

Example setup with visitor UUID resolved before the provider mounts:

import { QueryClientProvider } from "@tanstack/react-query";
import { AnalyticsProvider } from "@superbright/indexeddb-orm";
import { useVisitorSession } from "./hooks/useVisitorSession";
import { store } from "@superbright/indexeddb-orm";

// ── Outer shell – creates the QueryClient, cannot call hooks ──────────────
export function Providers({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<InnerProviders>{children}</InnerProviders>
</QueryClientProvider>
);
}

// ── Inner shell – lives inside QueryClientProvider, can call hooks ────────
function InnerProviders({ children }: { children: React.ReactNode }) {
// Reads visitor_uuid from IndexedDB; fetches from API and stores it if absent
const visitor_uuid = useVisitorSession(getSlugFromPathname());

return (
<AnalyticsProvider
visitor_uuid={visitor_uuid}
mixpanelToken={import.meta.env.VITE_MIXPANEL_TOKEN}
posthogKey={import.meta.env.VITE_POSTHOG_KEY}
posthogHost={import.meta.env.VITE_POSTHOG_HOST}
envMode={import.meta.env.MODE ?? "production"}
>
{children}
</AnalyticsProvider>
);
}

Getting started

  1. Wrap your React tree with AnalyticsProvider inside a component that has access to TanStack Query (i.e. inside QueryClientProvider).
  2. Resolve the visitor UUID before rendering by calling useVisitorSession (or store.getVisitorUUID() directly) and pass the result as the visitor_uuid prop. The provider uses this to call identify() on every configured service. Analytics initialisation is deferred until visitor_uuid is truthy, so no events are attributed to an anonymous session.
  3. Pass whichever service credentials you need — both are optional. If only mixpanelToken is provided, only Mixpanel is initialised; if only posthogKey is provided, only PostHog is initialised; both can run simultaneously.
  4. Call useTrackingEvents or useTrackEvent anywhere inside the provider to send events. Each call fans out to every configured service automatically.
  5. Reuse the exported interfaces when building strongly typed payloads.

All tracking helpers run payloads through Zod validators. Invalid payloads are silently dropped in production and logged as console.warn in development.

AnalyticsProvider

Component

React context provider that initialises Mixpanel and/or PostHog, identifies the current visitor on each service, and exposes a unified tracking interface through context. Debug logging is enabled when the environment is set to development.

Props

PropTypeDescription
childrenReact.ReactNodeReact subtree that can consume the analytics context.
visitor_uuidstring | undefinedStable visitor UUID sourced from IndexedDB (via store.getVisitorUUID() or useVisitorSession). Both Mixpanel and PostHog call identify() with this value once it is available. Analytics initialisation is deferred until this prop is truthy.
mixpanelTokenstring | undefinedMixpanel project token. When omitted, Mixpanel is not initialised.
posthogKeystring | undefinedPostHog project API key. When omitted, PostHog is not initialised.
posthogHoststring | undefinedPostHog API host. Defaults to https://us.i.posthog.com when not provided.
envModestring | undefinedOptional environment hint. "development" and "staging" log errors when tokens are missing; other values warn. Debug mode only enables during development.

Peer dependencies

Both analytics SDKs are optional peer dependencies. Install only the ones you need:

# Mixpanel only
pnpm add mixpanel-browser

# PostHog only
pnpm add posthog-js

# Both
pnpm add mixpanel-browser posthog-js

Hooks

useAnalytics

Returns the unified analytics client from context. The client exposes track and identify methods that fan out to every configured service.

const analytics = useAnalytics();
analytics.track("Page View", { page: "/search" });
analytics.identify("user-uuid");

useTrackEvent

Returns a callback that forwards eventName and optional data to all configured services. Useful for ad-hoc events.

const trackEvent = useTrackEvent();
trackEvent("CTA Click", { propertyId: "123", propertySlug: "eastside" });

useTrackingEvents

Provides a typed catalogue of high-level event helpers used throughout the search experience.

All payload properties are defined in camelCase for ergonomic TypeScript usage. The tracking layer automatically converts keys to snake_case before dispatching events, so downstream dashboards continue to receive the expected field names.

Key transform behaviour

Internally, payloads are normalised to camelCase for Zod validation, then converted to snake_case before dispatch. Both snake_case and camelCase inputs are accepted.

HelperDescriptionPayload type
trackViewAllUnitsFires when the user expands to see all units.ViewAllUnitsEvent
trackQuestionnaireSkipRecords skipping a questionnaire step.QuestionnaireEvent
trackQuestionnaireContinueRecords continuing a questionnaire step and selected answers.ContinueQuestionnaireEvent
trackQuestionnaireBackRecords navigating back in the questionnaire.QuestionnaireEvent
trackClickUnitTracks a unit card click in search results.ClickUnitEvent
trackHoverUnitTracks hovering a unit card.HoverUnitEvent
trackLoadMoreFired when the results pagination loads additional units.LoadMoreEvent
trackFilterOpenedLogged when the filter drawer/modal opens.FilterOpenEvent
trackFilterCanceledFired after filters are cleared/cancelled.FilterEvent
trackFilterAppliedFired when filters are confirmed.FilterEvent
trackOpenSortLogged when the sort control is opened.SortOpenEvent
trackSelectSortRecords a chosen sort option.SortEvent
trackCancelSortRecords cancelling the sort selection.SortEvent
trackClickUnitFavoritesTracks interactions within the favorites list.ClickUnitFavoritesEvent
trackUnitUnfavoritedFires when a unit is removed from favorites.UnitUnfavoritedEvent
trackQuestionnaireResultTracks the completion state of the questionnaire.QuestionnaireResultEvent
trackSimilarUnitsImpressionFires when similar units data is available and the UI renders the feature (API success + component visible). Only triggered when the user lands on 360 without an existing search query.SimilarUnitsImpressionEvent
trackSimilarUnitClickFires when a user clicks a unit from the similar units list.SimilarUnitClickEvent
trackSimilarUnitJumpSuccessFires when navigation to the selected unit completes successfully (new unit loads or 360 content is replaced).SimilarUnitJumpSuccessEvent
trackSimilarUnitJumpErrorFires when navigation fails or the user interacts before the system is ready (API not loaded, race condition, navigation error).SimilarUnitJumpErrorEvent

Functions

createStrictTracker

Low-level factory used internally by buildTrackingEvents. Creates a single typed tracker for a given event name and Zod schema.

createStrictTracker("My Event", mySchema)

Deprecated: skipKeyTransform has been removed. It was a no-op because convertKeysToSnakeCase is idempotent for snake_case inputs.

buildTrackingEvents

Creates the same catalogue of tracking helpers that useTrackingEvents returns, but accepts any compatible TrackEventFn. This is useful for testing or environments where React hooks are unavailable.

import { buildTrackingEvents } from "@superbright/indexeddb-orm";

const trackSpy = (event: string, data?: Record<string, unknown>) => {
console.log(event, data);
};

const analytics = buildTrackingEvents(trackSpy);
analytics.trackSelectSort({
propertyId: "123",
propertySlug: "main-st",
sortIndex: 0,
sortEnum: "relevance",
});

generateUserUUID

Legacy

generateUserUUID was the previous mechanism used internally by AnalyticsProvider to resolve visitor identity. The provider no longer calls it automatically. Pass visitor_uuid as a prop instead — see Visitor UUID below.


Visitor UUID

The ORM stores a stable visitor identifier under the "user" key in IndexedDB, nested as { visitor_uuid: "…" }. Two methods on the store singleton manage it directly.

import { store } from "@superbright/indexeddb-orm";

store.getVisitorUUID

const uuid = await store.getVisitorUUID();
// → string | null

Reads visitor_uuid from the "user" KV entry. Returns null when no UUID has been stored yet.

store.setVisitorUUID

await store.setVisitorUUID("550e8400-e29b-41d4-a716-446655440000");

Persists visitor_uuid inside the "user" KV entry, merging with any other fields already stored there.

For React apps, use the useVisitorSession hook instead of calling the store methods directly. It chains two TanStack Query queries:

  1. Read — calls store.getVisitorUUID() to check IndexedDB.
  2. Fetch — if the read returns null, calls POST /embed/{slug}/session to obtain a fresh UUID from the API and stores it via store.setVisitorUUID().

The resolved UUID (or undefined while loading) is returned and should be passed straight to AnalyticsProvider as visitor_uuid.

import { useQuery } from "@tanstack/react-query";
import { store } from "@superbright/indexeddb-orm";

export const useVisitorSession = (slug: string | undefined) => {
const storedQuery = useQuery({
queryKey: ["visitor_uuid"],
queryFn: () => store.getVisitorUUID(),
staleTime: Infinity,
cacheTime: Infinity,
});

const sessionQuery = useQuery({
queryKey: ["visitor-session", slug],
queryFn: async () => {
const response = await httpService.post(`embed/${slug}/session`, {});
const vid = response.data.visitor_uuid;
await store.setVisitorUUID(vid);
return vid;
},
// Only fetch from the API when IndexedDB confirmed no UUID is stored
enabled: !!slug && storedQuery.isSuccess && storedQuery.data === null,
staleTime: Infinity,
cacheTime: Infinity,
});

return storedQuery.data ?? sessionQuery.data ?? undefined;
};

Important: useVisitorSession calls useQuery, so it must be rendered inside a QueryClientProvider. Split your providers into an outer shell (creates QueryClientProvider) and an inner shell (calls the hook and renders AnalyticsProvider). See the Getting started example above.

Interfaces

TrackEventFn

Function signature consumed by buildTrackingEvents. Receives the event name and the (optionally) parsed payload.

ParameterTypeDescription
eventNamestringFully qualified event key.
dataRecord<string, any> | undefinedOptional payload that has already been validated by Zod schemas.

BaseEvent

PropertyTypeDescription
propertyIdstringIdentifier of the property a user is viewing (stringified).
propertySlugstringSlug used to represent the property in URLs or analytics.

ViewAllUnitsEvent

Extends BaseEvent.

PropertyTypeDescription
propertyNamestringHuman-friendly property name shown to renters.
totalUnitsAvailablenumberTotal units currently available at the property.
resultsUnitCountnumberCount of units returned in the latest results set.
unitCountDatestringISO timestamp when the counts were captured.

LoadMoreEvent

Extends BaseEvent.

PropertyTypeDescription
propertyNamestringHuman-friendly property name shown to renters.
totalUnitsAvailablenumberTotal units currently available at the property.
resultsUnitCountnumberCount of units currently displayed in the results list.
unitCountDatestringISO timestamp when the counts were captured.

QuestionnaireEvent

Extends BaseEvent.

PropertyTypeDescription
questionIndexnumberZero-based index of the question displayed to the visitor.
questionEnumQuestionEnumNormalized question category.

ContinueQuestionnaireEvent

Extends QuestionnaireEvent.

PropertyTypeDescription
selectedAnswersEnumArraystring[]All answer identifiers chosen for the current step.

UnitEvent

Extends BaseEvent.

PropertyTypeDescription
unitIdstringUnique identifier for the unit.
unitTitlestringHuman-readable unit title.
unitSlugstringSlug representing the unit in URLs/analytics.

ClickUnitEvent

Extends BaseEvent.

PropertyTypeDescription
propertyNamestringFriendly property name displayed to renters.
unitIdstringUnique identifier for the unit.
unitTitlestringHuman-readable unit title.
unitSlugstringSlug representing the unit in URLs/analytics.
favoritedUnitbooleanWhether the unit is favorited at the moment of the click.

HoverUnitEvent

Extends BaseEvent.

PropertyTypeDescription
propertyNamestringFriendly property name displayed to renters.
unitIdstringUnique identifier for the unit.
unitTitlestringHuman-readable unit title.
unitSlugstringSlug representing the unit being hovered.
favoritedUnitbooleanWhether the unit is favorited at the moment of the hover.

ClickUnitFavoritesEvent

Extends ClickUnitEvent.

PropertyTypeDescription
favoritedUnitbooleanIndicates whether the unit is favorited after the interaction.

FilterEvent

Extends BaseEvent.

PropertyTypeDescription
propertyNamestringHuman-friendly property name shown to renters.
filterIndexnumberPosition of the filter in the UI.
filterLabelstringDisplay label for the filter group.
selectedFiltersEnumstring[]Values applied when the event fires (coerced to strings).

FilterOpenEvent

Extends BaseEvent.

PropertyTypeDescription
propertyNamestringHuman-friendly property name shown to renters.
filterIndexnumberPosition of the filter in the UI.
filterLabelstringDisplay label for the filter.
selectedFiltersEnumstring[]Current selection when the filter is opened.
filterNamestringInternal filter key that was opened.

SortEvent

Extends BaseEvent.

PropertyTypeDescription
sortIndexnumberIndex of the selected sort option.
sortEnumSortEnumSort option identifier.

SortOpenEvent

Extends BaseEvent.

PropertyTypeDescription
propertyNamestringHuman-friendly property name shown to renters.
sortIndexnumberIndex of the selected sort option.
sortEnumSortEnumSort option identifier.
resultsUnitCountnumberCount of units currently visible when the sort panel opens.
unitCountDatestringISO timestamp captured when the sort panel opens.

QuestionnaireResultEvent

Extends BaseEvent.

PropertyTypeDescription
fullQuestionnaireSchemaJsonunknownRaw questionnaire payload persisted for debugging/analysis.
unitResults{ type?: string; total?: number; units?: { unitId: string; unit_name?: string }[] }Optional summary of the units returned after questionnaire completion.

TrackingEventWithUnits

Extends FilterEvent.

Adds a trackingEvent callback so consumers can replay the original filter event once unit data is available. Extra metadata may be attached (the type allows arbitrary keys) for downstream processing.

PropertyTypeDescription
trackingEvent(payload: FilterEvent) => voidDeferred analytics callback that should be invoked once unit data is available.
[metadata: string]unknownOptional additional metadata forwarded with the deferred event.

Enumerations

QuestionEnum

Represents the questionnaire steps that emit analytics events.

ValueDescription
"location"Tracks location-related questions.
"budget"Tracks budget questions.
"size"Tracks questions about size or bedrooms.
"other"Fallback for any additional questionnaire step.

FilterEnum

Alias of string. You can reuse more specific filter unions from your application if required.

SortEnum

ValueDescription
"cost_low_to_high"Sorts from lowest to highest price.
"cost_high_to_low"Sorts from highest to lowest price.
"newest"Highlights the most recent units.
"relevance"Sorts by the backend relevance score (default).

Example

import { QueryClientProvider } from "@tanstack/react-query";
import {
AnalyticsProvider,
store,
useTrackingEvents,
type ClickUnitEvent,
} from "@superbright/indexeddb-orm";
import { useVisitorSession } from "./hooks/useVisitorSession";

// ── Step 1: outer providers ───────────────────────────────────────────────
// QueryClientProvider must wrap InnerProviders so that useVisitorSession
// (which calls useQuery) has access to the QueryClient.
export function AppProviders({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<InnerProviders>{children}</InnerProviders>
</QueryClientProvider>
);
}

// ── Step 2: inner providers ───────────────────────────────────────────────
// Lives inside QueryClientProvider, so hooks are safe to call here.
function InnerProviders({ children }: { children: React.ReactNode }) {
// Reads visitor_uuid from IndexedDB; fetches from API and persists it if absent.
const visitor_uuid = useVisitorSession(getSlugFromPathname());

return (
<AnalyticsProvider
visitor_uuid={visitor_uuid}
mixpanelToken={import.meta.env.VITE_MIXPANEL_TOKEN}
posthogKey={import.meta.env.VITE_POSTHOG_KEY}
posthogHost={import.meta.env.VITE_POSTHOG_HOST}
envMode={import.meta.env.MODE ?? "production"}
>
{children}
</AnalyticsProvider>
);
}

// ── Step 3: read visitor UUID imperatively (non-React contexts) ───────────
// Use the store methods directly when hooks are not available.
const uuid = await store.getVisitorUUID(); // → string | null
await store.setVisitorUUID("550e8400-e29b-41d4-a716-446655440000");

// ── Step 4: track events anywhere inside AnalyticsProvider ────────────────
function UnitCard({ unit }: { unit: ClickUnitEvent }) {
const { trackClickUnit } = useTrackingEvents();

const handleClick = () => {
trackClickUnit({
propertyId: unit.propertyId,
propertySlug: unit.propertySlug,
propertyName: unit.propertyName,
unitId: unit.unitId,
unitSlug: unit.unitSlug,
unitTitle: unit.unitTitle,
favoritedUnit: unit.favoritedUnit,
});
};

return <button onClick={handleClick}>{unit.unitTitle}</button>;
}

By colocating analytics with the ORM, any future features (for example, stateful funnels or multi-property telemetry) can reuse the same interfaces and user identity helpers without additional setup.