@dtechph/wayfinding-core 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,9 @@
1
1
  # `@dtechph/wayfinding-core`
2
2
 
3
- Core API client for the Duon Wayfinding SDK. Fetches CMS-configured malls via `GET /v1/sdk/malls` using `x-api-key`.
3
+ Core API client + telemetry for the Duon Wayfinding SDK.
4
+
5
+ - `GET /v1/sdk/malls` (with `x-api-key`)
6
+ - Batched `POST /v1/sdk/analytics/events` (session, search, location, geofence, …)
4
7
 
5
8
  ## Install
6
9
 
@@ -8,8 +11,6 @@ Core API client for the Duon Wayfinding SDK. Fetches CMS-configured malls via `G
8
11
  npm install @dtechph/wayfinding-core
9
12
  ```
10
13
 
11
- Requires GitHub Packages auth — see the [root README](../../README.md).
12
-
13
14
  ## Usage
14
15
 
15
16
  ```typescript
@@ -18,10 +19,17 @@ import { DuonWayfinding } from "@dtechph/wayfinding-core";
18
19
  DuonWayfinding.initialize({
19
20
  apiBaseUrl: "https://your-backend.example.com",
20
21
  apiKey: "duon_sk_live_...",
22
+ platform: "react-native",
21
23
  });
22
24
 
23
25
  const malls = await DuonWayfinding.fetchMalls();
24
- const url = DuonWayfinding.getViewerUrl(malls[0]);
26
+ // Situm malls may include situmApiKey / situmBuildingId
27
+ DuonWayfinding.setActiveMall(malls[0]);
28
+
29
+ // Escape-hatch tracking (native MapView wires these automatically):
30
+ DuonWayfinding.trackSearch({ query: "coffee", resultType: "poi" });
31
+ await DuonWayfinding.flush();
25
32
  ```
26
33
 
27
34
  For React Native / Expo UI components, use [`@dtechph/wayfinding-react-native`](../react-native).
35
+ See the [root README](../../README.md) for Situm vs kiosk auto-track behavior and CMS reporting.
package/dist/client.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { DuonAuthError, DuonConfigError, DuonForbiddenError, DuonNetworkError, } from "./errors";
2
2
  import { mapApiMall } from "./mall";
3
+ import { configureTelemetry, resetTelemetryForTests, startTelemetrySession, } from "./telemetry";
3
4
  let config = null;
4
5
  const normalizeBaseUrl = (url) => url.replace(/\/$/, "");
5
6
  export const initialize = (next) => {
@@ -12,7 +13,14 @@ export const initialize = (next) => {
12
13
  config = {
13
14
  apiBaseUrl: normalizeBaseUrl(next.apiBaseUrl.trim()),
14
15
  apiKey: next.apiKey.trim(),
16
+ platform: next.platform,
17
+ sdkVersion: next.sdkVersion,
15
18
  };
19
+ configureTelemetry({
20
+ platform: next.platform,
21
+ sdkVersion: next.sdkVersion,
22
+ });
23
+ startTelemetrySession();
16
24
  };
17
25
  export const getConfig = () => {
18
26
  if (!config) {
@@ -60,4 +68,5 @@ export const fetchMalls = async (fetchImpl = fetch) => {
60
68
  };
61
69
  export const resetForTests = () => {
62
70
  config = null;
71
+ resetTelemetryForTests();
63
72
  };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,75 @@
1
1
  import { fetchMalls, getConfig, initialize, resetForTests } from "./client";
2
2
  import { getViewerUrl } from "./mall";
3
- export { initialize, fetchMalls, getConfig, resetForTests, getViewerUrl };
3
+ import { endTelemetrySession, flush, getActiveMall, getClientSessionId, setActiveMall, trackEvent, trackGeofenceEnter, trackGeofenceExit, trackLocation, trackLocationBatch, trackMapError, trackMapLoad, trackPoiSelect, trackSearch } from "./telemetry";
4
+ export { initialize, fetchMalls, getConfig, resetForTests, getViewerUrl, setActiveMall, flush, trackEvent, trackSearch, trackLocation, trackLocationBatch, trackGeofenceEnter, trackGeofenceExit, trackPoiSelect, trackMapLoad, trackMapError, endTelemetrySession, getClientSessionId, getActiveMall, };
4
5
  export type { DuonMall, DuonWayfindingConfig, MallMapType } from "./types";
6
+ export type { SdkAnalyticsEventType, TrackEventInput, TelemetryPlatform, } from "./telemetry";
5
7
  export { DuonError, DuonAuthError, DuonConfigError, DuonForbiddenError, DuonNetworkError, } from "./errors";
6
8
  export declare const DuonWayfinding: {
7
9
  initialize: (next: import("./types").DuonWayfindingConfig) => void;
8
10
  fetchMalls: (fetchImpl?: import("./client").FetchFn) => Promise<import("./types").DuonMall[]>;
9
11
  getViewerUrl: (mall: import("./types").DuonMall) => string;
12
+ setActiveMall: (mall: import("./types").DuonMall) => void;
13
+ flush: () => Promise<void>;
14
+ trackEvent: (input: import("./telemetry").TrackEventInput) => void;
15
+ trackSearch: (input: {
16
+ query: string;
17
+ resultType?: string;
18
+ resultId?: string;
19
+ resultName?: string;
20
+ resultCount?: number;
21
+ buildingId?: string;
22
+ floorId?: string;
23
+ }) => void;
24
+ trackLocation: (input: {
25
+ x?: number;
26
+ y?: number;
27
+ lat?: number;
28
+ lng?: number;
29
+ accuracy?: number;
30
+ floorId?: string;
31
+ buildingId?: string;
32
+ }) => void;
33
+ trackLocationBatch: (samples: Array<{
34
+ x?: number;
35
+ y?: number;
36
+ lat?: number;
37
+ lng?: number;
38
+ accuracy?: number;
39
+ floorId?: string;
40
+ buildingId?: string;
41
+ clientTimestamp?: string;
42
+ }>) => void;
43
+ trackGeofenceEnter: (input: {
44
+ geofenceId: string;
45
+ geofenceName?: string;
46
+ buildingId?: string;
47
+ floorId?: string;
48
+ code?: string;
49
+ }) => void;
50
+ trackGeofenceExit: (input: {
51
+ geofenceId: string;
52
+ geofenceName?: string;
53
+ buildingId?: string;
54
+ floorId?: string;
55
+ code?: string;
56
+ }) => void;
57
+ trackPoiSelect: (input: {
58
+ poiId: string;
59
+ poiName?: string;
60
+ buildingId?: string;
61
+ floorId?: string;
62
+ categoryId?: string;
63
+ }) => void;
64
+ trackMapLoad: (input?: {
65
+ buildingId?: string;
66
+ mapType?: string;
67
+ }) => void;
68
+ trackMapError: (input: {
69
+ message: string;
70
+ buildingId?: string;
71
+ }) => void;
72
+ endTelemetrySession: () => void;
73
+ getClientSessionId: () => string;
74
+ getActiveMall: () => import("./types").DuonMall | null;
10
75
  };
package/dist/index.js CHANGED
@@ -1,9 +1,24 @@
1
1
  import { fetchMalls, getConfig, initialize, resetForTests } from "./client";
2
2
  import { getViewerUrl } from "./mall";
3
- export { initialize, fetchMalls, getConfig, resetForTests, getViewerUrl };
3
+ import { endTelemetrySession, flush, getActiveMall, getClientSessionId, setActiveMall, trackEvent, trackGeofenceEnter, trackGeofenceExit, trackLocation, trackLocationBatch, trackMapError, trackMapLoad, trackPoiSelect, trackSearch, } from "./telemetry";
4
+ export { initialize, fetchMalls, getConfig, resetForTests, getViewerUrl, setActiveMall, flush, trackEvent, trackSearch, trackLocation, trackLocationBatch, trackGeofenceEnter, trackGeofenceExit, trackPoiSelect, trackMapLoad, trackMapError, endTelemetrySession, getClientSessionId, getActiveMall, };
4
5
  export { DuonError, DuonAuthError, DuonConfigError, DuonForbiddenError, DuonNetworkError, } from "./errors";
5
6
  export const DuonWayfinding = {
6
7
  initialize,
7
8
  fetchMalls,
8
9
  getViewerUrl,
10
+ setActiveMall,
11
+ flush,
12
+ trackEvent,
13
+ trackSearch,
14
+ trackLocation,
15
+ trackLocationBatch,
16
+ trackGeofenceEnter,
17
+ trackGeofenceExit,
18
+ trackPoiSelect,
19
+ trackMapLoad,
20
+ trackMapError,
21
+ endTelemetrySession,
22
+ getClientSessionId,
23
+ getActiveMall,
9
24
  };
package/dist/mall.js CHANGED
@@ -12,6 +12,10 @@ export const mapApiMall = (raw) => {
12
12
  mapEmbedUrl: raw.map_embed_url,
13
13
  latitude: raw.latitude,
14
14
  longitude: raw.longitude,
15
+ situmApiKey: raw.situm_api_key,
16
+ situmBuildingId: raw.situm_building_id
17
+ ? String(raw.situm_building_id)
18
+ : undefined,
15
19
  };
16
20
  };
17
21
  export const getViewerUrl = (mall) => mall.viewerUrl;
@@ -0,0 +1,82 @@
1
+ import type { DuonMall } from "./types";
2
+ export type SdkAnalyticsEventType = "session_start" | "session_end" | "mall_selected" | "map_load" | "map_error" | "search" | "poi_select" | "location_sample" | "geofence_enter" | "geofence_exit";
3
+ export interface TrackEventInput {
4
+ type: SdkAnalyticsEventType;
5
+ buildingId?: string;
6
+ mallId?: string;
7
+ floorId?: string;
8
+ payload?: Record<string, unknown>;
9
+ clientTimestamp?: string;
10
+ }
11
+ export type TelemetryPlatform = "react-native" | "flutter" | "web" | string;
12
+ export declare const configureTelemetry: (options: {
13
+ platform?: TelemetryPlatform;
14
+ sdkVersion?: string;
15
+ fetchImpl?: typeof fetch;
16
+ }) => void;
17
+ export declare const getClientSessionId: () => string;
18
+ export declare const getActiveMall: () => DuonMall | null;
19
+ /** Call from initialize — starts anonymous session + session_start event. */
20
+ export declare const startTelemetrySession: () => void;
21
+ export declare const endTelemetrySession: () => void;
22
+ export declare const setActiveMall: (mall: DuonMall) => void;
23
+ export declare const trackEvent: (input: TrackEventInput) => void;
24
+ export declare const trackSearch: (input: {
25
+ query: string;
26
+ resultType?: string;
27
+ resultId?: string;
28
+ resultName?: string;
29
+ resultCount?: number;
30
+ buildingId?: string;
31
+ floorId?: string;
32
+ }) => void;
33
+ export declare const trackLocation: (input: {
34
+ x?: number;
35
+ y?: number;
36
+ lat?: number;
37
+ lng?: number;
38
+ accuracy?: number;
39
+ floorId?: string;
40
+ buildingId?: string;
41
+ }) => void;
42
+ export declare const trackLocationBatch: (samples: Array<{
43
+ x?: number;
44
+ y?: number;
45
+ lat?: number;
46
+ lng?: number;
47
+ accuracy?: number;
48
+ floorId?: string;
49
+ buildingId?: string;
50
+ clientTimestamp?: string;
51
+ }>) => void;
52
+ export declare const trackGeofenceEnter: (input: {
53
+ geofenceId: string;
54
+ geofenceName?: string;
55
+ buildingId?: string;
56
+ floorId?: string;
57
+ code?: string;
58
+ }) => void;
59
+ export declare const trackGeofenceExit: (input: {
60
+ geofenceId: string;
61
+ geofenceName?: string;
62
+ buildingId?: string;
63
+ floorId?: string;
64
+ code?: string;
65
+ }) => void;
66
+ export declare const trackPoiSelect: (input: {
67
+ poiId: string;
68
+ poiName?: string;
69
+ buildingId?: string;
70
+ floorId?: string;
71
+ categoryId?: string;
72
+ }) => void;
73
+ export declare const trackMapLoad: (input?: {
74
+ buildingId?: string;
75
+ mapType?: string;
76
+ }) => void;
77
+ export declare const trackMapError: (input: {
78
+ message: string;
79
+ buildingId?: string;
80
+ }) => void;
81
+ export declare const flush: () => Promise<void>;
82
+ export declare const resetTelemetryForTests: () => void;
@@ -0,0 +1,255 @@
1
+ import { getConfig } from "./client";
2
+ const FLUSH_INTERVAL_MS = 5000;
3
+ const MAX_QUEUE = 100;
4
+ const LOCATION_MIN_INTERVAL_MS = 1000;
5
+ let clientSessionId = null;
6
+ let platform = "web";
7
+ let sdkVersion = "0.2.0";
8
+ let queue = [];
9
+ let flushTimer = null;
10
+ let flushing = false;
11
+ let lastLocationAt = 0;
12
+ let fetchImpl = fetch;
13
+ let activeMall = null;
14
+ let sessionStarted = false;
15
+ const uuid = () => {
16
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
17
+ return crypto.randomUUID();
18
+ }
19
+ return `sess-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
20
+ };
21
+ const ensureSession = () => {
22
+ if (!clientSessionId) {
23
+ clientSessionId = uuid();
24
+ }
25
+ return clientSessionId;
26
+ };
27
+ const startFlushTimer = () => {
28
+ if (flushTimer)
29
+ return;
30
+ flushTimer = setInterval(() => {
31
+ void flush();
32
+ }, FLUSH_INTERVAL_MS);
33
+ };
34
+ export const configureTelemetry = (options) => {
35
+ if (options.platform)
36
+ platform = options.platform;
37
+ if (options.sdkVersion)
38
+ sdkVersion = options.sdkVersion;
39
+ if (options.fetchImpl)
40
+ fetchImpl = options.fetchImpl;
41
+ };
42
+ export const getClientSessionId = () => ensureSession();
43
+ export const getActiveMall = () => activeMall;
44
+ /** Call from initialize — starts anonymous session + session_start event. */
45
+ export const startTelemetrySession = () => {
46
+ ensureSession();
47
+ startFlushTimer();
48
+ if (!sessionStarted) {
49
+ sessionStarted = true;
50
+ enqueue({ type: "session_start", payload: {} });
51
+ }
52
+ };
53
+ export const endTelemetrySession = () => {
54
+ enqueue({ type: "session_end", payload: {} });
55
+ void flush();
56
+ sessionStarted = false;
57
+ };
58
+ export const setActiveMall = (mall) => {
59
+ activeMall = mall;
60
+ enqueue({
61
+ type: "mall_selected",
62
+ buildingId: mall.buildingId,
63
+ payload: {
64
+ mapType: mall.mapType,
65
+ name: mall.name,
66
+ viewerHost: (() => {
67
+ try {
68
+ return new URL(mall.viewerUrl).host;
69
+ }
70
+ catch {
71
+ return undefined;
72
+ }
73
+ })(),
74
+ },
75
+ });
76
+ };
77
+ const enqueue = (input) => {
78
+ ensureSession();
79
+ startFlushTimer();
80
+ const buildingId = input.buildingId ?? activeMall?.buildingId;
81
+ queue.push({
82
+ type: input.type,
83
+ building_id: buildingId,
84
+ mall_id: input.mallId,
85
+ floor_id: input.floorId,
86
+ payload: input.payload ?? {},
87
+ client_timestamp: input.clientTimestamp ?? new Date().toISOString(),
88
+ });
89
+ if (queue.length >= MAX_QUEUE) {
90
+ void flush();
91
+ }
92
+ };
93
+ export const trackEvent = (input) => {
94
+ enqueue(input);
95
+ };
96
+ export const trackSearch = (input) => {
97
+ enqueue({
98
+ type: "search",
99
+ buildingId: input.buildingId,
100
+ floorId: input.floorId,
101
+ payload: {
102
+ query: input.query,
103
+ resultType: input.resultType ?? "unknown",
104
+ resultId: input.resultId,
105
+ resultName: input.resultName,
106
+ resultCount: input.resultCount,
107
+ },
108
+ });
109
+ };
110
+ export const trackLocation = (input) => {
111
+ const now = Date.now();
112
+ if (now - lastLocationAt < LOCATION_MIN_INTERVAL_MS)
113
+ return;
114
+ lastLocationAt = now;
115
+ enqueue({
116
+ type: "location_sample",
117
+ buildingId: input.buildingId,
118
+ floorId: input.floorId,
119
+ payload: {
120
+ x: input.x,
121
+ y: input.y,
122
+ lat: input.lat,
123
+ lng: input.lng,
124
+ accuracy: input.accuracy,
125
+ floorId: input.floorId,
126
+ buildingId: input.buildingId,
127
+ },
128
+ });
129
+ };
130
+ export const trackLocationBatch = (samples) => {
131
+ for (const s of samples) {
132
+ enqueue({
133
+ type: "location_sample",
134
+ buildingId: s.buildingId,
135
+ floorId: s.floorId,
136
+ clientTimestamp: s.clientTimestamp,
137
+ payload: {
138
+ x: s.x,
139
+ y: s.y,
140
+ lat: s.lat,
141
+ lng: s.lng,
142
+ accuracy: s.accuracy,
143
+ floorId: s.floorId,
144
+ buildingId: s.buildingId,
145
+ },
146
+ });
147
+ }
148
+ };
149
+ export const trackGeofenceEnter = (input) => {
150
+ enqueue({
151
+ type: "geofence_enter",
152
+ buildingId: input.buildingId,
153
+ floorId: input.floorId,
154
+ payload: {
155
+ geofenceId: input.geofenceId,
156
+ geofenceName: input.geofenceName,
157
+ buildingId: input.buildingId,
158
+ floorId: input.floorId,
159
+ code: input.code,
160
+ },
161
+ });
162
+ };
163
+ export const trackGeofenceExit = (input) => {
164
+ enqueue({
165
+ type: "geofence_exit",
166
+ buildingId: input.buildingId,
167
+ floorId: input.floorId,
168
+ payload: {
169
+ geofenceId: input.geofenceId,
170
+ geofenceName: input.geofenceName,
171
+ buildingId: input.buildingId,
172
+ floorId: input.floorId,
173
+ code: input.code,
174
+ },
175
+ });
176
+ };
177
+ export const trackPoiSelect = (input) => {
178
+ enqueue({
179
+ type: "poi_select",
180
+ buildingId: input.buildingId,
181
+ floorId: input.floorId,
182
+ payload: {
183
+ resultId: input.poiId,
184
+ resultName: input.poiName,
185
+ resultType: "poi",
186
+ categoryId: input.categoryId,
187
+ },
188
+ });
189
+ };
190
+ export const trackMapLoad = (input) => {
191
+ enqueue({
192
+ type: "map_load",
193
+ buildingId: input?.buildingId,
194
+ payload: { mapType: input?.mapType },
195
+ });
196
+ };
197
+ export const trackMapError = (input) => {
198
+ enqueue({
199
+ type: "map_error",
200
+ buildingId: input.buildingId,
201
+ payload: { message: input.message },
202
+ });
203
+ };
204
+ export const flush = async () => {
205
+ if (flushing || queue.length === 0)
206
+ return;
207
+ let apiBaseUrl;
208
+ let apiKey;
209
+ try {
210
+ ({ apiBaseUrl, apiKey } = getConfig());
211
+ }
212
+ catch {
213
+ return;
214
+ }
215
+ flushing = true;
216
+ const batch = queue.splice(0, MAX_QUEUE);
217
+ try {
218
+ const response = await fetchImpl(`${apiBaseUrl}/v1/sdk/analytics/events`, {
219
+ method: "POST",
220
+ headers: {
221
+ Accept: "application/json",
222
+ "Content-Type": "application/json",
223
+ "x-api-key": apiKey,
224
+ },
225
+ body: JSON.stringify({
226
+ client_session_id: ensureSession(),
227
+ platform,
228
+ sdk_version: sdkVersion,
229
+ events: batch,
230
+ }),
231
+ });
232
+ if (!response.ok) {
233
+ // Put back on failure (best-effort)
234
+ queue.unshift(...batch);
235
+ }
236
+ }
237
+ catch {
238
+ queue.unshift(...batch);
239
+ }
240
+ finally {
241
+ flushing = false;
242
+ }
243
+ };
244
+ export const resetTelemetryForTests = () => {
245
+ clientSessionId = null;
246
+ queue = [];
247
+ flushing = false;
248
+ lastLocationAt = 0;
249
+ activeMall = null;
250
+ sessionStarted = false;
251
+ if (flushTimer) {
252
+ clearInterval(flushTimer);
253
+ flushTimer = null;
254
+ }
255
+ };
package/dist/types.d.ts CHANGED
@@ -7,10 +7,16 @@ export interface DuonMall {
7
7
  mapEmbedUrl?: string;
8
8
  latitude?: number;
9
9
  longitude?: number;
10
+ /** Present for Situm malls from authenticated SDK mall list. */
11
+ situmApiKey?: string;
12
+ situmBuildingId?: string;
10
13
  }
11
14
  export interface DuonWayfindingConfig {
12
15
  apiBaseUrl: string;
13
16
  apiKey: string;
17
+ /** Optional override; defaults by platform package. */
18
+ platform?: string;
19
+ sdkVersion?: string;
14
20
  }
15
21
  export interface MallsApiResponse {
16
22
  status: number;
@@ -22,5 +28,7 @@ export interface MallsApiResponse {
22
28
  map_embed_url?: string;
23
29
  latitude?: number;
24
30
  longitude?: number;
31
+ situm_api_key?: string;
32
+ situm_building_id?: string;
25
33
  }>;
26
34
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dtechph/wayfinding-core",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Duon Wayfinding SDK — core API client for CMS-driven mall maps",
5
5
  "license": "UNLICENSED",
6
6
  "author": "dtechph",