wovvmap_analytics
v0.0.8
Published
`wovvmap_analytics` is a small, framework-independent bridge between a map application and the WovvMap analytics backend.
Downloads
784
Readme
wovvmap_analytics
wovvmap_analytics is a small, framework-independent bridge between a map
application and the WovvMap analytics backend.
The current fresh-start version supports:
session_startedsession_heartbeatsession_endeddestination_selectedorigin_selectedsearch_performed,search_result_selected, andsearch_abandoned- navigation journey lifecycle events
filter_selection_changedspatial_heatmap_batchfor bounded floor-wise map click and dwell cells- strongly typed application UI interactions such as
ui_element_clicked client_error_recordedandanalytics_delivery_issue_reportedfor System Health
It does not store data locally and does not contain dashboard UI.
Load mall viewer configuration
The same package also acts as the typed read bridge for remotely managed viewer configuration. Call it once when a mall is loaded:
import { getMallViewerConfiguration } from 'wovvmap_analytics'
const configuration = await getMallViewerConfiguration({
mallId: '87954445085211',
environment: 'sit',
apiVersion: 'v2',
})
const showZoomIn =
configuration.templates.kioskClassic.actions.zoom_in
const defaultTheme = configuration.theme.themes.find(
(theme) => theme.id === configuration.theme.defaultThemeId,
)
const lightTokens = defaultTheme?.modes.find(
(definition) => definition.mode === 'light',
)?.tokensThe exact configuration key is environment:apiVersion:mallId, so matching
SIT and production mall IDs never share settings accidentally. The response
contains allowed/default templates, the complete allowed theme catalog with
mode-wise ViewerThemeTokens, and a direct action-visibility record for every
template. Theme IDs, labels and token JSON come from the backend; consumers do
not need a duplicated static theme list or token file. Consumers should combine
a remote action rule with runtime availability; for example, the Offers action
still requires actual offer data.
The package validates the response, caches it for 60 seconds and revalidates with ETag. If the backend is temporarily unavailable, it uses the latest cached configuration or a safe built-in fallback. It does not persist configuration in IndexedDB or local storage.
Future layout control will extend the same versioned response with stable component IDs, layout slots and bounded design tokens. Integrations must not accept raw CSS or executable code from the backend.
Global application setup
For complete application analytics, create one client above the application's root navigation. This does not depend on the map WebView, so Login and other native/web screens can send events before a mall is opened:
import {
AnalyticsProvider,
backendProvider,
createAnalytics,
} from 'wovvmap_analytics'
const analytics = createAnalytics({
application: {
id: 'nexus_one',
name: 'Nexus One',
version: '2.1.0',
},
contextProvider: async () => ({
authenticationStatus: 'guest',
externalUserId: null,
userName: null,
deviceId: 'stable-device-id',
deviceIdType: 'native_device',
devicePlatform: 'ios',
deviceModel: 'iPhone',
containerType: 'native_app',
}),
autoTrack: {
errors: true,
},
providers: [backendProvider()],
})
export function AppRoot() {
return (
<AnalyticsProvider client={analytics}>
<App />
</AnalyticsProvider>
)
}The integrating application owns its identity and runtime context. Product names are therefore not hard-coded in package services.
Track semantic actions from any component through the provider:
const analytics = useAnalytics()
await analytics.track('ui_element_clicked', {
screenId: 'login',
routeName: 'Login',
elementId: 'login_submit',
elementLabel: 'Proceed',
elementType: 'button',
flowId: 'authentication.login',
flowVariant: 'email_password',
metadata: {
loginMethod: 'email_password',
},
})Use flowId for the stable business flow and flowVariant when the same
route/action has multiple paths, such as phone OTP and email/password. Never
put passwords, tokens, phone numbers or email addresses in metadata.
System Health integration
Set autoTrack.errors: true on the application-level client to capture browser
errors, unhandled promise rejections, and React Native global errors. The same
application session and runtime context are attached automatically, so the
dashboard can filter incidents by application, version, visitor, device, and
container. Call analytics.stopAutoTracking() only when the application root
is permanently unmounted.
Map integrations can also report an error with the currently active mall session:
await mapAnalytics.recordClientError({
...activeSessionContext,
severity: 'error',
category: 'javascript',
errorName: error.name,
errorCode: null,
message: error.message,
stack: error.stack ?? null,
})Use reportAnalyticsDeliveryIssue when a host owns retry/offline delivery and
knows that an analytics request failed or later recovered. The backend also
records malformed and rejected /api/analytics/events requests by itself.
Health tracking is best-effort and must never replace the host application's
normal error handler.
One typed event pipeline
All built-in events are delivered to one backend endpoint:
POST /api/analytics/eventsThe endpoint is shared, but event payloads are not merged into one loose
object. eventType is the discriminator for a strongly typed event map:
type WovvMapAnalyticsEventMap = {
session_started: MallSessionEvent
session_heartbeat: MallSessionHeartbeatEvent
session_ended: MallSessionEndedEvent
origin_selected: OriginSelectionEvent
destination_selected: DestinationSelectionEvent
filter_selection_changed: FilterSelectionEvent
spatial_heatmap_batch: SpatialHeatmapBatchEvent
client_error_recorded: ClientErrorRecordedEvent
analytics_delivery_issue_reported: AnalyticsDeliveryIssueReportedEvent
// Navigation lifecycle event types are registered here as well.
}Each domain keeps its own controller, service, validator, and database read
model. The backend event registry selects the correct domain processor without
a growing if/else chain. Adding an event means:
- define its event name and payload type;
- implement its domain validation and persistence;
- register its processor;
- expose a clear package method for application developers.
The public endpoint and transport do not change when another event is added.
Track spatial heatmap activity
Use trackSpatialHeatmapBatch for already-aggregated map grid cells. Do not
send every pointer move. The integration should sample movement, combine
nearby coordinates, and send no more than 50 cells per request:
await analytics.trackSpatialHeatmapBatch({
sessionId: session.sessionId,
surface: 'map',
cells: [
{
cellKey: '3:21:17:map',
floorIndex: 3,
floorName: 'Ground Floor',
gridX: 21,
gridY: 17,
mapX: 2580,
mapY: 2100,
normalizedX: 0.52,
normalizedY: 0.47,
clickCount: 1,
dwellTimeMs: 750,
sampleCount: 3,
nodePointKey: null,
},
],
})mapX and mapY must use the unscaled map coordinate system, not camera or
screen pixels. normalizedX and normalizedY are diagnostic viewport values
between 0 and 1. The WovVMap core integration performs this batching and
coordinate conversion automatically, including when the viewer runs inside a
WebView or iframe. The existing mall session supplies application, user,
device, container and mall identity; the host must not send a duplicate event.
Installation
npm install wovvmap_analyticsStart a mall session
Create the analytics client once in your integration service. The backend URL is centrally managed inside the package:
import { createWovvMapAnalytics } from 'wovvmap_analytics'
const analytics = createWovvMapAnalytics()Call startMallSession after the selected mall has loaded successfully:
const session = await analytics.startMallSession({
applicationSessionId: analytics.applicationSessionId,
clientInstanceId: 'viewer-instance-id',
mallId: '87954445085211',
mallApiVersion: 'v2',
mallEnvironment: 'sit',
authenticationStatus: 'authenticated',
externalUserId: 'user-123',
userName: 'Alex',
deviceId: 'stable-device-id',
deviceIdType: 'native_device',
devicePlatform: 'ios',
deviceModel: 'iPhone',
sourceApplicationId: 'nexus_one',
sourceApplicationName: 'Nexus One',
sourceApplicationVersion: '1.0.0',
containerType: 'webview',
})applicationSessionId is the parent application lifecycle. Keep it unchanged
while the host application remains open. Every map visit still receives its own
sessionId, allowing one application session to contain multiple mall visits
without losing login-to-map correlation.
While the map is visible, confirm that the session is still alive. The WovVMap core integration sends this every 30 seconds:
await analytics.heartbeatMallSession({
sessionId: session.sessionId,
})Keep only one heartbeat timer per active session. Pause it while the viewer is hidden/backgrounded and stop it before ending the session. A heartbeat updates the existing session row; it does not create a separate analytics row.
Keep the returned sessionId and end the same session when its owning map
screen closes, loses focus, changes mall, or moves to the background:
await analytics.endMallSession({
sessionId: session.sessionId,
endReason: 'screen_unfocused',
})Supported end reasons are:
screen_unfocusedapp_backgroundedmall_changedidentity_changedpage_closeduser_logoutinactivity_timeoutsession_replacedmanual
The backend closes the existing session row and stores endedAt,
durationMs, endReason, and the end event ID. Repeated end requests are
idempotent and do not create duplicate session rows. inactivity_timeout and
session_replaced are normally assigned by the backend.
If a browser, iframe, or WebView disappears without sending session_ended,
the backend expires it after the configured timeout. Starting a new session
with the same clientInstanceId also closes the previous instance without
affecting another tab or WebView.
Track a destination selection
Call trackDestinationSelection only after the viewer has resolved the selected
destination node and has an active mall session:
await analytics.trackDestinationSelection({
sessionId: session.sessionId,
destinationNodeId: 'node-101',
destinationNodeIds: ['node-101'],
destinationName: 'Example Store',
amenityId: null,
brandId: 'brand-10',
categoryCode: 'fashion',
subCategoryCode: 'menswear',
floorIndex: 1,
floorName: 'First Floor',
selectionSource: 'search',
})For grouped stores, pass every location in destinationNodeIds and the displayed
location first as destinationNodeId. interactionId is optional; the package
creates it automatically. An iframe or WebView host should provide one stable
interaction ID through the bridge when a host-side action selects the destination.
The backend uses it to make retries idempotent.
Supported selection sources are map, search, category, subcategory,
amenity, favorite, offer, offer_with_category,
offer_with_subcategory, deeplink, direction, ai_assistant, bridge,
and unknown.
For an amenity selection, pass its stable ID and use the amenity source:
amenityId: 'amenity-10',
selectionSource: 'amenity',Track an origin selection
Use trackOriginSelection for a meaningful start-point selection. Do not call
it for history restoration or other internal state synchronization:
await analytics.trackOriginSelection({
sessionId: session.sessionId,
originNodeId: 'entrance-node-1',
originNodeIds: ['entrance-node-1'],
originName: 'Main Entrance',
amenityId: null,
brandId: null,
categoryCode: null,
subCategoryCode: null,
floorIndex: 0,
floorName: 'Ground Floor',
selectionSource: 'default_location',
isDefaultLocation: true,
})Supported sources are direction_search, default_location, deeplink,
amenity, ai_assistant, bridge, swap, map, and unknown.
amenityId is required when the source is amenity. A default location can
also contain an amenity ID. The backend counts the same default origin once per
session and keeps one current assignment per mall and device.
Track a navigation journey
Use one stable journeyId for a single direction request. Increment
sequenceNumber for every meaningful lifecycle change and repeat the immutable
origin, destination, floor, and route snapshot on each event.
await analytics.trackNavigationJourneyEvent({
sessionId: session.sessionId,
journeyId: 'journey-123',
sourceSearchId: null,
sourceOriginSearchId: null,
sourceDestinationSearchId: null,
simulationRunId: null,
simulationRunNumber: null,
sequenceNumber: 1,
eventType: 'direction_requested',
originNodeId: 'entrance-1',
originName: 'Main Entrance',
originFloorIndex: 0,
originFloorName: 'Ground Floor',
destinationNodeId: 'store-101',
destinationName: 'Example Store',
destinationFloorIndex: 1,
destinationFloorName: 'First Floor',
pathType: 'default',
fallbackUsed: null,
routeDistanceMeters: null,
estimatedDurationSeconds: null,
stepCount: 0,
floorTransitionCount: 0,
progress: 0,
currentStepIndex: null,
currentFloorIndex: 0,
failureReason: null,
endReason: null,
})Supported lifecycle events are:
direction_requested,route_generated, androute_failed;simulation_started,simulation_resumed, andsimulation_paused;simulation_progress_milestoneandsimulation_completed;simulation_abandonedandstep_by_step_opened;route_replacedandroute_cleared.
Send progress milestones at meaningful boundaries such as 25%, 50%, and 75%.
Do not send animation-frame or every-position events. The backend keeps an
append-only event timeline and updates one current summary row per journey.
Retries are idempotent by eventId and by journeyId + sequenceNumber.
One journey may contain multiple simulation attempts. Create a new
simulationRunId and increment simulationRunNumber only when Play starts a
new attempt. Pause, resume, progress, completion, abandonment, and step-list
events for that attempt reuse the same run identity. Route-level events use
null for both fields. Clearing the route ends the journey; generating the
same route after that clear creates a new journeyId.
When the WovvMap viewer core is used, its integration owns this lifecycle tracking. An iframe or WebView host should only issue navigation commands and must not call the analytics backend again for the same action.
When a session ends while a simulation is active or paused, the backend adds a
system simulation_abandoned timeline event. A generated route that was never
started is finalized as not_started. Completed journeys remain completed.
Track search intent
Use one searchId for the complete lifecycle of a stable query. Send the
performed event after the UI debounce has settled, then reuse its ID when the
visitor selects a result or closes the search without selecting anything:
const search = await analytics.trackSearchPerformed({
sessionId: session.sessionId,
queryText: 'coffee',
resultCount: 4,
inputSource: 'main_search',
intentType: 'store',
topResult: {
nodeId: 'store-101',
name: 'Coffee House',
brandId: 'brand-10',
categoryCode: 'food',
amenityId: null,
},
})
await analytics.trackSearchResultSelected({
sessionId: session.sessionId,
searchId: search.searchId,
resultPosition: 1,
selectedResult: {
nodeId: 'store-101',
name: 'Coffee House',
brandId: 'brand-10',
categoryCode: 'food',
amenityId: null,
},
})Supported input sources are main_search, origin, destination,
assistant, and other. Supported intent types are store, brand,
category, amenity, general, and unresolved.
When selected search results create a direction request, pass their IDs as
sourceOriginSearchId and sourceDestinationSearchId. This links both start
and end-point searches to the same journey without a dashboard-side join. The
viewer consumes each attribution after the next matching route, so later manual
routes to the same nodes are not incorrectly counted as search conversions.
sourceSearchId remains temporarily available for older destination-only
integrations.
Do not send an event for every keystroke. The integrating UI should debounce
input and suppress unchanged normalized queries. Only user-originated input
changes should call trackSearchPerformed; values filled from URL parameters,
default locations, or store selections must not be reported as searches merely
because they appear in the input. The backend stores one transaction summary
per search plus an append-only event timeline; retries are idempotent by
eventId.
If the search input is rendered by a native app or iframe host, do not call
this package from both the host and viewer. Send the typed search lifecycle
through wovvmap-webview-bridge's single typed analyticsEvent channel; the
viewer will attach its active mall/session context and make the backend call.
The package sends this backend-ready contract:
interface MallSessionEvent {
schemaVersion: '1.1'
eventId: string
eventType: 'session_started'
sessionId: string
clientInstanceId: string
mallId: string
mallApiVersion: 'v1' | 'v2'
mallEnvironment: 'sit' | 'prod'
authenticationStatus: 'guest' | 'authenticated'
externalUserId: string | null
userName: string | null
deviceId: string
deviceIdType:
| 'browser_installation'
| 'native_device'
| 'managed_device'
| 'kiosk_configured'
devicePlatform:
| 'android'
| 'ios'
| 'windows'
| 'macos'
| 'linux'
| 'web'
| 'kiosk'
deviceModel: string | null
sourceApplicationId: string
sourceApplicationName: string
sourceApplicationVersion: string | null
containerType: 'browser' | 'webview' | 'iframe' | 'kiosk'
occurredAt: string
}eventId, sessionId, and occurredAt are created by the package. The host
application supplies the mall, identity, device, and source application
context. A host should provide a stable clientInstanceId for one browser tab,
iframe, or WebView across reloads. For guest sessions, pass
externalUserId: null and userName: null.
Integration responsibility
The host application should:
- create one analytics client;
- call the event only from the common successful mall-load flow;
- provide a stable device/installation ID appropriate for its platform;
- identify the host with a stable application ID and readable name;
- maintain one stable client-instance ID per tab, iframe, or WebView;
- send one heartbeat every 30 seconds while that viewer is active;
- stop the heartbeat before sending
session_ended; - handle network errors without breaking the map experience.
The package sends all analytics events directly to the analytics backend. It does not store sessions locally, render dashboard UI, or send a second copy through the native/iframe host.
Multi-application reporting
sourceApplicationId must be a stable machine-readable product ID and
sourceApplicationName must be its readable label. Do not hard-code a single
client or product inside a reusable service.
Examples:
| Product branch | Application ID | Application name |
| --- | --- | --- |
| Nexus One | nexus_one | Nexus One |
| OKEN | oken | OKEN |
| Oberoi | oberoi | Oberoi |
Sessions store this application context once. Origin, destination, journey, and future UI events inherit the canonical context from the session when their read-model rows are written. This supports combined reports and application, version, environment, user, device, container, mall, and date filters without client-side joins.
The OKEN multi-product application resolves this identity through
AppAnalyticsContextService; adding a product belongs in the central
application analytics configuration, not in the service.
