@metricinsights/ca-analytics 0.1.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/dist/index.cjs ADDED
@@ -0,0 +1,633 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ENTITY_NAME: () => ENTITY_NAME,
24
+ VERSION: () => VERSION,
25
+ init: () => init,
26
+ isActive: () => isActive,
27
+ shutdown: () => shutdown,
28
+ track: () => track,
29
+ trackElement: () => trackElement,
30
+ trackPageView: () => trackPageView,
31
+ useAnalytics: () => useAnalytics
32
+ });
33
+ module.exports = __toCommonJS(index_exports);
34
+
35
+ // src/core/constants.ts
36
+ var ENTITY_NAME = "analytics_events";
37
+ var VERSION = "0.1.0";
38
+ var MI_PAGE_PATH_RE = /^\/(p[tl]?)\/([^/]+)(.*)$/;
39
+ var WIDTHS = {
40
+ app: 100,
41
+ event: 100,
42
+ page_path: 400,
43
+ version: 20,
44
+ meta: 2e3
45
+ };
46
+ var RESERVED_EVENTS = ["page_view", "heartbeat", "component_render", "element_click"];
47
+ var EVENT_NAME_RE = /^[a-z][a-z0-9_]{0,99}$/;
48
+ var LIMITS = { referrer: 300, query: 300 };
49
+
50
+ // src/core/config.ts
51
+ var UNSUBSTITUTED_RE = /^\[[A-Za-z0-9][A-Za-z0-9 _-]*\]$/;
52
+ function portalVariables() {
53
+ if (typeof window === "undefined") return {};
54
+ const block = window.PP_VARIABLES;
55
+ return block && typeof block === "object" && !Array.isArray(block) ? block : {};
56
+ }
57
+ function readFlag(key) {
58
+ const raw = portalVariables()[key];
59
+ if (typeof raw === "boolean") return raw;
60
+ if (typeof raw !== "string") return void 0;
61
+ const value = raw.trim().toLowerCase();
62
+ if (value === "" || UNSUBSTITUTED_RE.test(raw.trim())) return void 0;
63
+ return value !== "0" && value !== "false" && value !== "n" && value !== "no";
64
+ }
65
+ function matchPage() {
66
+ if (typeof window === "undefined" || !window.location) return null;
67
+ return MI_PAGE_PATH_RE.exec(window.location.pathname);
68
+ }
69
+ function currentPagePath() {
70
+ const match = matchPage();
71
+ if (!match) return "/";
72
+ const rest = match[3];
73
+ return (rest && rest !== "" && rest !== "/" ? rest : "/").slice(0, WIDTHS.page_path);
74
+ }
75
+ function resolveConfig(options = {}) {
76
+ const enabled = options.enabled ?? readFlag("CUSTOM_APP_ANALYTICS_ENABLED") ?? true;
77
+ if (!enabled) return null;
78
+ const app = (options.app ?? matchPage()?.[2] ?? "").slice(0, WIDTHS.app);
79
+ if (!app) return null;
80
+ return { app, endpoint: `/data/page/${encodeURIComponent(app)}/${ENTITY_NAME}` };
81
+ }
82
+
83
+ // src/core/event.ts
84
+ var pad2 = (n) => String(n).padStart(2, "0");
85
+ function toId(value) {
86
+ const n = Number(value);
87
+ return Number.isFinite(n) ? Math.trunc(n) : 0;
88
+ }
89
+ function formatTs(date) {
90
+ return `${date.getUTCFullYear()}-${pad2(date.getUTCMonth() + 1)}-${pad2(date.getUTCDate())} ${pad2(date.getUTCHours())}:${pad2(date.getUTCMinutes())}:${pad2(date.getUTCSeconds())}`;
91
+ }
92
+ function stringify(value) {
93
+ try {
94
+ const out = JSON.stringify(value);
95
+ return typeof out === "string" ? out : null;
96
+ } catch {
97
+ return null;
98
+ }
99
+ }
100
+ function serializeMeta(meta) {
101
+ if (!meta) return "{}";
102
+ const full = stringify(meta);
103
+ if (full !== null && full.length <= WIDTHS.meta) return full;
104
+ const kept = {};
105
+ for (const [key, value] of Object.entries(meta)) {
106
+ const candidate = stringify({ ...kept, [key]: value, _truncated: 1 });
107
+ if (candidate === null || candidate.length > WIDTHS.meta) continue;
108
+ kept[key] = value;
109
+ }
110
+ return stringify({ ...kept, _truncated: 1 }) ?? "{}";
111
+ }
112
+ function uuid() {
113
+ const c = typeof crypto !== "undefined" ? crypto : void 0;
114
+ if (c && typeof c.randomUUID === "function") return c.randomUUID();
115
+ const bytes = new Uint8Array(16);
116
+ if (c && typeof c.getRandomValues === "function") {
117
+ c.getRandomValues(bytes);
118
+ } else {
119
+ for (let i = 0; i < bytes.length; i += 1) bytes[i] = Math.floor(Math.random() * 256);
120
+ }
121
+ bytes[6] = bytes[6] & 15 | 64;
122
+ bytes[8] = bytes[8] & 63 | 128;
123
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
124
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
125
+ }
126
+ function createRow(input) {
127
+ return {
128
+ id: uuid(),
129
+ ts: formatTs(/* @__PURE__ */ new Date()),
130
+ app: input.app.slice(0, WIDTHS.app),
131
+ event: input.event.slice(0, WIDTHS.event),
132
+ session_id: input.sessionId.slice(0, 36),
133
+ page_path: input.pagePath.slice(0, WIDTHS.page_path),
134
+ element_id: toId(input.elementId),
135
+ version: VERSION.slice(0, WIDTHS.version),
136
+ meta: serializeMeta(input.meta)
137
+ };
138
+ }
139
+
140
+ // src/core/transport.ts
141
+ var RETRY_DELAY_MS = 2e3;
142
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
143
+ var toArrayBody = (rows) => `[${rows.join(",")}]`;
144
+ async function post(endpoint, body) {
145
+ try {
146
+ const response = await fetch(endpoint, {
147
+ method: "POST",
148
+ credentials: "same-origin",
149
+ headers: { "Content-Type": "application/json" },
150
+ body
151
+ });
152
+ if (!response.ok) {
153
+ return { ok: false, retryable: response.status === 429 || response.status >= 500 };
154
+ }
155
+ const payload = await response.json().catch(() => null);
156
+ if (payload && typeof payload.resultCode === "number" && payload.resultCode !== 0) {
157
+ return { ok: false, retryable: false };
158
+ }
159
+ return { ok: true, retryable: false };
160
+ } catch {
161
+ return { ok: false, retryable: true };
162
+ }
163
+ }
164
+ async function send(endpoint, rows) {
165
+ if (rows.length === 0) return "ok";
166
+ const body = toArrayBody(rows);
167
+ const first = await post(endpoint, body);
168
+ if (first.ok) return "ok";
169
+ if (!first.retryable) return "failed";
170
+ await sleep(RETRY_DELAY_MS);
171
+ return (await post(endpoint, body)).ok ? "ok" : "failed";
172
+ }
173
+ function sendOnUnload(endpoint, rows) {
174
+ if (rows.length === 0) return;
175
+ const body = toArrayBody(rows);
176
+ try {
177
+ const blob = new Blob([body], { type: "application/json" });
178
+ if (navigator.sendBeacon && navigator.sendBeacon(endpoint, blob)) return;
179
+ } catch {
180
+ }
181
+ try {
182
+ void fetch(endpoint, {
183
+ method: "POST",
184
+ credentials: "same-origin",
185
+ headers: { "Content-Type": "application/json" },
186
+ body,
187
+ keepalive: true
188
+ });
189
+ } catch {
190
+ }
191
+ }
192
+
193
+ // src/core/queue.ts
194
+ var FLUSH_INTERVAL_MS = 15e3;
195
+ var MAX_FLUSH_ROWS = 50;
196
+ var MAX_FLUSH_BYTES = 3e4;
197
+ var MAX_CONSECUTIVE_FAILURES = 3;
198
+ var textEncoder = new TextEncoder();
199
+ function createQueue(endpoint, onKilled) {
200
+ let buffer = [];
201
+ let bytes = 0;
202
+ let failures = 0;
203
+ let stopped = false;
204
+ let inFlight = false;
205
+ let killed = false;
206
+ const timer = setInterval(() => {
207
+ void flush();
208
+ }, FLUSH_INTERVAL_MS);
209
+ function stop() {
210
+ stopped = true;
211
+ buffer = [];
212
+ bytes = 0;
213
+ clearInterval(timer);
214
+ }
215
+ async function flush() {
216
+ if (stopped || inFlight || buffer.length === 0) return;
217
+ const batch = buffer;
218
+ buffer = [];
219
+ bytes = 0;
220
+ inFlight = true;
221
+ try {
222
+ let outcome;
223
+ try {
224
+ outcome = await send(
225
+ endpoint,
226
+ batch.map((b) => b.json)
227
+ );
228
+ } catch {
229
+ outcome = "failed";
230
+ }
231
+ if (outcome === "ok") {
232
+ failures = 0;
233
+ return;
234
+ }
235
+ failures += 1;
236
+ if (failures >= MAX_CONSECUTIVE_FAILURES && !killed) {
237
+ killed = true;
238
+ stop();
239
+ try {
240
+ onKilled?.();
241
+ } catch {
242
+ }
243
+ }
244
+ } finally {
245
+ inFlight = false;
246
+ }
247
+ }
248
+ return {
249
+ push(row) {
250
+ if (stopped) return;
251
+ let json;
252
+ try {
253
+ json = JSON.stringify(row);
254
+ } catch {
255
+ return;
256
+ }
257
+ const size = textEncoder.encode(json).length;
258
+ buffer.push({ json, size });
259
+ bytes += size;
260
+ if (buffer.length >= MAX_FLUSH_ROWS || bytes >= MAX_FLUSH_BYTES) void flush();
261
+ },
262
+ flush() {
263
+ void flush();
264
+ },
265
+ flushOnUnload() {
266
+ if (stopped || buffer.length === 0) return;
267
+ const batch = buffer;
268
+ buffer = [];
269
+ bytes = 0;
270
+ sendOnUnload(
271
+ endpoint,
272
+ batch.map((b) => b.json)
273
+ );
274
+ },
275
+ stop
276
+ };
277
+ }
278
+
279
+ // src/core/session.ts
280
+ var SID_KEY = "ca_pa_sid";
281
+ var LAST_KEY = "ca_pa_last";
282
+ var IDLE_MS = 30 * 60 * 1e3;
283
+ var TOUCH_THROTTLE_MS = 5e3;
284
+ var memoryId = null;
285
+ var lastWrittenAt = 0;
286
+ function read(key) {
287
+ try {
288
+ return localStorage.getItem(key);
289
+ } catch {
290
+ return null;
291
+ }
292
+ }
293
+ function write(key, value) {
294
+ try {
295
+ localStorage.setItem(key, value);
296
+ } catch {
297
+ }
298
+ }
299
+ function newId() {
300
+ try {
301
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
302
+ } catch {
303
+ }
304
+ return `s-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
305
+ }
306
+ function resetSession() {
307
+ memoryId = null;
308
+ lastWrittenAt = 0;
309
+ }
310
+ function getSessionId() {
311
+ const now = Date.now();
312
+ const storedId = read(SID_KEY);
313
+ const rawLast = read(LAST_KEY);
314
+ const last = rawLast === null ? Number.NaN : Number(rawLast);
315
+ const alive = Number.isFinite(last) && now - last <= IDLE_MS;
316
+ let id;
317
+ if (storedId && alive) {
318
+ id = storedId;
319
+ } else if (!storedId && memoryId) {
320
+ id = memoryId;
321
+ } else {
322
+ id = newId();
323
+ write(SID_KEY, id);
324
+ lastWrittenAt = 0;
325
+ }
326
+ memoryId = id;
327
+ if (now - lastWrittenAt >= TOUCH_THROTTLE_MS) {
328
+ write(LAST_KEY, String(now));
329
+ lastWrittenAt = now;
330
+ }
331
+ return id;
332
+ }
333
+
334
+ // src/core/signals/heartbeat.ts
335
+ var HEARTBEAT_FAST_MS = 6e4;
336
+ var HEARTBEAT_SLOW_MS = 3e5;
337
+ var HEARTBEAT_FAST_COUNT = 5;
338
+ function startHeartbeat(emit) {
339
+ let beats = 0;
340
+ let timer;
341
+ let stopped = false;
342
+ const interval = () => beats < HEARTBEAT_FAST_COUNT ? HEARTBEAT_FAST_MS : HEARTBEAT_SLOW_MS;
343
+ function schedule() {
344
+ if (stopped || timer !== void 0) return;
345
+ timer = setTimeout(() => {
346
+ timer = void 0;
347
+ if (stopped || document.visibilityState !== "visible") return;
348
+ beats += 1;
349
+ try {
350
+ emit();
351
+ } catch {
352
+ }
353
+ schedule();
354
+ }, interval());
355
+ }
356
+ function clear() {
357
+ if (timer !== void 0) {
358
+ clearTimeout(timer);
359
+ timer = void 0;
360
+ }
361
+ }
362
+ const onVisibilityChange = () => {
363
+ if (stopped) return;
364
+ if (document.visibilityState === "visible") {
365
+ schedule();
366
+ } else {
367
+ clear();
368
+ }
369
+ };
370
+ document.addEventListener("visibilitychange", onVisibilityChange);
371
+ if (document.visibilityState === "visible") schedule();
372
+ return {
373
+ stop() {
374
+ stopped = true;
375
+ clear();
376
+ document.removeEventListener("visibilitychange", onVisibilityChange);
377
+ }
378
+ };
379
+ }
380
+
381
+ // src/core/signals/miRender.ts
382
+ var RENDER_CAP = 50;
383
+ function startMiRender(emit) {
384
+ let stopped = false;
385
+ const counts = /* @__PURE__ */ new Map();
386
+ const onRender = (event) => {
387
+ if (stopped) return;
388
+ try {
389
+ const detail = event.detail;
390
+ if (!detail || typeof detail !== "object") return;
391
+ const component = typeof detail.component === "string" ? detail.component : "";
392
+ if (!component) return;
393
+ const props = detail.props && typeof detail.props === "object" ? detail.props : {};
394
+ const elementId = toId(props.element ?? props.element_id ?? 0);
395
+ const segmentId = toId(props.segment ?? props.segment_value_id ?? 0);
396
+ const key = `${component}|${elementId}|${segmentId}`;
397
+ const seq = (counts.get(key) ?? 0) + 1;
398
+ if (seq > RENDER_CAP) return;
399
+ counts.set(key, seq);
400
+ const meta = {
401
+ component,
402
+ element_id: elementId,
403
+ segment_id: segmentId
404
+ };
405
+ if (seq === RENDER_CAP) meta.capped = 1;
406
+ emit({ elementId, meta });
407
+ } catch {
408
+ }
409
+ };
410
+ document.addEventListener("mi-render", onRender);
411
+ return {
412
+ stop() {
413
+ stopped = true;
414
+ document.removeEventListener("mi-render", onRender);
415
+ counts.clear();
416
+ }
417
+ };
418
+ }
419
+
420
+ // src/core/signals/pageview.ts
421
+ var inboundReferrer = () => {
422
+ try {
423
+ const raw = document.referrer;
424
+ if (!raw) return "";
425
+ const url = new URL(raw);
426
+ return `${url.origin}${url.pathname}`;
427
+ } catch {
428
+ return "";
429
+ }
430
+ };
431
+ function startPageView(emit, currentPath) {
432
+ let stopped = false;
433
+ let lastPath = null;
434
+ let lastLocation = null;
435
+ const locationKey = (path) => {
436
+ try {
437
+ return `${path}${window.location.search}`;
438
+ } catch {
439
+ return path;
440
+ }
441
+ };
442
+ const safeEmit = (resolvePath, dedupe) => {
443
+ if (stopped) return;
444
+ try {
445
+ const path = resolvePath();
446
+ const location = locationKey(path);
447
+ if (dedupe && location === lastLocation) return;
448
+ const referrer = lastPath === null ? inboundReferrer() : lastPath;
449
+ lastPath = path;
450
+ lastLocation = location;
451
+ emit(path, referrer);
452
+ } catch {
453
+ }
454
+ };
455
+ const originalPush = window.history.pushState;
456
+ const originalReplace = window.history.replaceState;
457
+ const patch = (original) => function patched(...args) {
458
+ const result = original.apply(this, args);
459
+ safeEmit(currentPath, true);
460
+ return result;
461
+ };
462
+ const patchedPush = patch(originalPush);
463
+ const patchedReplace = patch(originalReplace);
464
+ window.history.pushState = patchedPush;
465
+ window.history.replaceState = patchedReplace;
466
+ const onPopState = () => safeEmit(currentPath, true);
467
+ window.addEventListener("popstate", onPopState);
468
+ safeEmit(currentPath, true);
469
+ return {
470
+ stop() {
471
+ stopped = true;
472
+ lastPath = null;
473
+ lastLocation = null;
474
+ if (window.history.pushState === patchedPush) {
475
+ window.history.pushState = originalPush;
476
+ }
477
+ if (window.history.replaceState === patchedReplace) {
478
+ window.history.replaceState = originalReplace;
479
+ }
480
+ window.removeEventListener("popstate", onPopState);
481
+ },
482
+ emitNow(path) {
483
+ safeEmit(() => path ?? currentPath(), false);
484
+ }
485
+ };
486
+ }
487
+
488
+ // src/core/analytics.ts
489
+ var runtime = null;
490
+ var warnedOnce = /* @__PURE__ */ new Set();
491
+ function guard(fn) {
492
+ try {
493
+ fn();
494
+ } catch {
495
+ }
496
+ }
497
+ function warnOnce(key, message) {
498
+ if (warnedOnce.has(key)) return;
499
+ warnedOnce.add(key);
500
+ console.warn(`[ca-analytics] ${message}`);
501
+ }
502
+ function pageViewMeta(referrer) {
503
+ const query = window.location.search.replace(/^\?/, "");
504
+ return { referrer: referrer.slice(0, LIMITS.referrer), query: query.slice(0, LIMITS.query) };
505
+ }
506
+ function stopStarted(parts, flushFirst) {
507
+ if (flushFirst) guard(() => parts.queue.flushOnUnload());
508
+ guard(() => parts.pageView?.stop());
509
+ guard(() => parts.stopHeartbeat?.());
510
+ guard(() => parts.stopMiRender?.());
511
+ guard(() => parts.detachUnload());
512
+ guard(() => parts.queue.stop());
513
+ }
514
+ function teardown(flushFirst) {
515
+ const current = runtime;
516
+ if (!current) return;
517
+ runtime = null;
518
+ warnedOnce.clear();
519
+ stopStarted(current, flushFirst);
520
+ resetSession();
521
+ }
522
+ function init(options = {}) {
523
+ guard(() => {
524
+ if (runtime) return;
525
+ const config = resolveConfig(options);
526
+ if (!config) return;
527
+ const queue = createQueue(config.endpoint, () => shutdown());
528
+ const emit = (event, extra = {}) => {
529
+ queue.push(
530
+ createRow({
531
+ app: config.app,
532
+ event,
533
+ sessionId: getSessionId(),
534
+ pagePath: extra.pagePath ?? currentPagePath(),
535
+ elementId: extra.elementId,
536
+ meta: extra.meta
537
+ })
538
+ );
539
+ };
540
+ const onUnload = () => guard(() => queue.flushOnUnload());
541
+ const onVisibility = () => {
542
+ if (document.visibilityState === "hidden") onUnload();
543
+ };
544
+ const detachUnload = () => {
545
+ window.removeEventListener("pagehide", onUnload);
546
+ document.removeEventListener("visibilitychange", onVisibility);
547
+ };
548
+ let pageView;
549
+ let stopHeartbeat;
550
+ let stopMiRender;
551
+ try {
552
+ window.addEventListener("pagehide", onUnload);
553
+ document.addEventListener("visibilitychange", onVisibility);
554
+ stopHeartbeat = startHeartbeat(() => emit("heartbeat")).stop;
555
+ stopMiRender = startMiRender(
556
+ (payload) => emit("component_render", { elementId: payload.elementId, meta: payload.meta })
557
+ ).stop;
558
+ pageView = startPageView(
559
+ (path, referrer) => emit("page_view", { pagePath: path, meta: pageViewMeta(referrer) }),
560
+ currentPagePath
561
+ );
562
+ } catch (error) {
563
+ stopStarted({ queue, detachUnload, pageView, stopHeartbeat, stopMiRender }, false);
564
+ throw error;
565
+ }
566
+ runtime = { queue, emit, detachUnload, pageView, stopHeartbeat, stopMiRender };
567
+ });
568
+ }
569
+ function track(event, props) {
570
+ guard(() => {
571
+ if (!runtime) return;
572
+ if (!event) return;
573
+ const normalized = event.toLowerCase();
574
+ if (RESERVED_EVENTS.includes(normalized)) {
575
+ warnOnce("reserved", `"${event}" is reserved and was not tracked (further occurrences are silent)`);
576
+ return;
577
+ }
578
+ if (!EVENT_NAME_RE.test(event)) {
579
+ warnOnce("name", `"${event}" is not snake_case \u2264100 chars; tracked as-is (further occurrences are silent)`);
580
+ }
581
+ runtime.emit(event, { meta: props });
582
+ });
583
+ }
584
+ function trackElement(props) {
585
+ guard(() => {
586
+ if (!runtime) return;
587
+ const { element_id, segment_id, ...extra } = props ?? {};
588
+ const id = toId(element_id);
589
+ if (!id) {
590
+ warnOnce("element", `trackElement() ignored a non-finite or zero element_id (${element_id})`);
591
+ return;
592
+ }
593
+ const segmentRaw = Number(segment_id);
594
+ const segmentFinite = Number.isFinite(segmentRaw);
595
+ if (segment_id !== void 0 && !segmentFinite) {
596
+ warnOnce("segment", `trackElement() got a non-finite segment_id (${segment_id}); recorded as 0`);
597
+ }
598
+ const meta = {
599
+ ...extra,
600
+ element_id: id,
601
+ segment_id: segmentFinite ? Math.trunc(segmentRaw) : 0
602
+ };
603
+ runtime.emit("element_click", { elementId: id, meta });
604
+ });
605
+ }
606
+ function trackPageView(path) {
607
+ guard(() => runtime?.pageView.emitNow(path));
608
+ }
609
+ function shutdown() {
610
+ guard(() => teardown(true));
611
+ }
612
+ function isActive() {
613
+ return runtime !== null;
614
+ }
615
+
616
+ // src/react/useAnalytics.ts
617
+ var api = { track, trackElement, trackPageView };
618
+ function useAnalytics() {
619
+ return api;
620
+ }
621
+ // Annotate the CommonJS export names for ESM import in node:
622
+ 0 && (module.exports = {
623
+ ENTITY_NAME,
624
+ VERSION,
625
+ init,
626
+ isActive,
627
+ shutdown,
628
+ track,
629
+ trackElement,
630
+ trackPageView,
631
+ useAnalytics
632
+ });
633
+ //# sourceMappingURL=index.cjs.map