@founderhq/journeys 0.4.1 → 0.4.2

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
@@ -208,6 +208,12 @@ Capture is enabled by default after access validation succeeds. Pass `capture={f
208
208
 
209
209
  `apiKey`, `journeyId`, and the FounderHQ API URL are inherited from the root props and are not repeated inside `capture`.
210
210
 
211
+ Capture events are batched and persisted locally with a TTL. The runtime flushes
212
+ immediately for completion events, and browser embeds also try an exit-safe
213
+ `sendBeacon`/`keepalive` flush on `visibilitychange` and `pagehide`. Beacon
214
+ delivery is treated as best-effort; FounderHQ uses event IDs server-side so a
215
+ later retry can safely duplicate a queued event.
216
+
211
217
  ## Config Shape
212
218
 
213
219
  A journey config is centered around:
package/dist/index.cjs CHANGED
@@ -9693,20 +9693,15 @@ function JourneyShell({ className, theme } = {}) {
9693
9693
  ] });
9694
9694
  }
9695
9695
  var JOURNEY_LIBRARY_NAME = "@founderhq/journeys";
9696
- var JOURNEY_LIBRARY_VERSION = "0.4.1";
9696
+ var JOURNEY_LIBRARY_VERSION = "0.4.2";
9697
+ var DEFAULT_QUEUE_TTL_MS = 24 * 60 * 60 * 1e3;
9698
+ var MAX_QUEUED_CAPTURE_EVENTS = 100;
9699
+ var MAX_BEACON_BYTES = 60 * 1024;
9697
9700
  function randomId(prefix) {
9698
9701
  const cryptoRef = globalThis.crypto;
9699
9702
  if (cryptoRef == null ? void 0 : cryptoRef.randomUUID) return `${prefix}_${cryptoRef.randomUUID()}`;
9700
9703
  return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
9701
9704
  }
9702
- function readJson(storage, key, fallback) {
9703
- try {
9704
- const raw = storage.getItem(key);
9705
- return raw ? JSON.parse(raw) : fallback;
9706
- } catch (e) {
9707
- return fallback;
9708
- }
9709
- }
9710
9705
  function getStoredId(storage, key, prefix) {
9711
9706
  try {
9712
9707
  const existing = storage.getItem(key);
@@ -9718,6 +9713,64 @@ function getStoredId(storage, key, prefix) {
9718
9713
  return randomId(prefix);
9719
9714
  }
9720
9715
  }
9716
+ function safeBrowserStorage(name) {
9717
+ var _a;
9718
+ if (typeof window === "undefined") return null;
9719
+ try {
9720
+ return (_a = window[name]) != null ? _a : null;
9721
+ } catch (e) {
9722
+ return null;
9723
+ }
9724
+ }
9725
+ function isQueuedCaptureEvent(value) {
9726
+ if (!isRecord(value)) return false;
9727
+ const event = value.event;
9728
+ return isRecord(event) && typeof event.id === "string" && typeof event.type === "string" && typeof event.occurredAt === "string" && typeof value.attempts === "number" && typeof value.createdAt === "number";
9729
+ }
9730
+ function pruneQueuedEvents(queue, now = Date.now(), ttlMs = DEFAULT_QUEUE_TTL_MS) {
9731
+ const cutoff = now - ttlMs;
9732
+ return queue.filter((item) => item.createdAt >= cutoff);
9733
+ }
9734
+ function readPersistedCaptureQueue(storage, key) {
9735
+ if (!storage) return null;
9736
+ try {
9737
+ const raw = storage.getItem(key);
9738
+ if (!raw) return null;
9739
+ const parsed = JSON.parse(raw);
9740
+ if (typeof parsed.visitorId !== "string" || typeof parsed.clientSessionId !== "string") {
9741
+ return null;
9742
+ }
9743
+ const queue = Array.isArray(parsed.queue) ? pruneQueuedEvents(parsed.queue.filter(isQueuedCaptureEvent)) : [];
9744
+ return {
9745
+ visitorId: parsed.visitorId,
9746
+ clientSessionId: parsed.clientSessionId,
9747
+ updatedAt: typeof parsed.updatedAt === "number" ? parsed.updatedAt : Date.now(),
9748
+ queue
9749
+ };
9750
+ } catch (e) {
9751
+ return null;
9752
+ }
9753
+ }
9754
+ function persistCaptureQueue(storage, runtime, queue) {
9755
+ if (!storage) return;
9756
+ try {
9757
+ const pruned = pruneQueuedEvents(queue).slice(-MAX_QUEUED_CAPTURE_EVENTS);
9758
+ if (pruned.length === 0) {
9759
+ storage.removeItem(runtime.queueKey);
9760
+ return;
9761
+ }
9762
+ storage.setItem(
9763
+ runtime.queueKey,
9764
+ JSON.stringify({
9765
+ visitorId: runtime.visitorId,
9766
+ clientSessionId: runtime.clientSessionId,
9767
+ updatedAt: Date.now(),
9768
+ queue: pruned
9769
+ })
9770
+ );
9771
+ } catch (e) {
9772
+ }
9773
+ }
9721
9774
  function isRecord(value) {
9722
9775
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
9723
9776
  }
@@ -9818,6 +9871,54 @@ function captureContext(capture, runtime) {
9818
9871
  screen: screenRef ? { width: screenRef.width, height: screenRef.height } : void 0
9819
9872
  }, customContext));
9820
9873
  }
9874
+ function captureUrl(capture) {
9875
+ var _a;
9876
+ const baseUrl = (_a = capture.baseUrl) != null ? _a : "https://getfounderhq.com";
9877
+ return `${baseUrl}/api/v1/journeys/${encodeURIComponent(
9878
+ capture.journeyId
9879
+ )}/capture`;
9880
+ }
9881
+ function byteLength(value) {
9882
+ try {
9883
+ return new TextEncoder().encode(value).byteLength;
9884
+ } catch (e) {
9885
+ return value.length;
9886
+ }
9887
+ }
9888
+ function safeSendBeacon() {
9889
+ if (typeof navigator === "undefined") return null;
9890
+ const beacon = navigator.sendBeacon;
9891
+ return typeof beacon === "function" ? beacon.bind(navigator) : null;
9892
+ }
9893
+ async function sendCaptureBatch(capture, runtime, batch, options = {}) {
9894
+ const body = JSON.stringify(__spreadProps(__spreadValues({}, options.preferBeacon ? { apiKey: capture.apiKey } : {}), {
9895
+ clientSessionId: runtime.clientSessionId,
9896
+ visitorId: runtime.visitorId,
9897
+ context: captureContext(capture, runtime),
9898
+ events: batch.map((item) => item.event)
9899
+ }));
9900
+ const url = captureUrl(capture);
9901
+ if (options.preferBeacon && byteLength(body) <= MAX_BEACON_BYTES) {
9902
+ const beacon = safeSendBeacon();
9903
+ if (beacon == null ? void 0 : beacon(url, body)) {
9904
+ return "queued";
9905
+ }
9906
+ }
9907
+ try {
9908
+ const response = await fetch(url, {
9909
+ method: "POST",
9910
+ headers: {
9911
+ Authorization: `Bearer ${capture.apiKey}`,
9912
+ "Content-Type": "application/json"
9913
+ },
9914
+ body,
9915
+ keepalive: batch.length <= 5 || options.preferBeacon === true
9916
+ });
9917
+ return response.ok ? "acknowledged" : "failed";
9918
+ } catch (e) {
9919
+ return "failed";
9920
+ }
9921
+ }
9821
9922
  function toJsonRecord(value) {
9822
9923
  return isRecord(value) ? value : {};
9823
9924
  }
@@ -9963,30 +10064,34 @@ function useJourneyCapture(params) {
9963
10064
  captureRef.current = params.capture;
9964
10065
  const persistQueue = React.useCallback(() => {
9965
10066
  const runtime = runtimeRef.current;
9966
- if (!runtime || typeof window === "undefined") return;
9967
- try {
9968
- sessionStorage.setItem(
9969
- runtime.queueKey,
9970
- JSON.stringify(queueRef.current)
9971
- );
9972
- } catch (e) {
9973
- }
10067
+ if (!runtime) return;
10068
+ persistCaptureQueue(
10069
+ safeBrowserStorage("localStorage"),
10070
+ runtime,
10071
+ queueRef.current
10072
+ );
9974
10073
  }, []);
9975
10074
  const ensureRuntime = React.useCallback((capture) => {
10075
+ var _a, _b;
9976
10076
  if (typeof window === "undefined") return null;
10077
+ const queueKey = `fhq_journey_queue:v2:${capture.apiKey.slice(
10078
+ 0,
10079
+ 16
10080
+ )}:${capture.journeyId}`;
9977
10081
  const existing = runtimeRef.current;
9978
- if ((existing == null ? void 0 : existing.journeyId) === capture.journeyId) return existing;
9979
- const visitorId = getStoredId(
9980
- localStorage,
9981
- "fhq_journey_visitor_id",
9982
- "fhqv"
9983
- );
9984
- const clientSessionId = getStoredId(
9985
- sessionStorage,
9986
- `fhq_journey_session:${capture.journeyId}`,
9987
- "fhqs"
9988
- );
9989
- const queueKey = `fhq_journey_queue:${capture.journeyId}:${clientSessionId}`;
10082
+ if ((existing == null ? void 0 : existing.journeyId) === capture.journeyId && existing.queueKey === queueKey) {
10083
+ return existing;
10084
+ }
10085
+ const local = safeBrowserStorage("localStorage");
10086
+ const session = safeBrowserStorage("sessionStorage");
10087
+ const persisted = readPersistedCaptureQueue(local, queueKey);
10088
+ const visitorId = (_a = persisted == null ? void 0 : persisted.visitorId) != null ? _a : local ? getStoredId(local, "fhq_journey_visitor_id", "fhqv") : randomId("fhqv");
10089
+ const sessionKey = `fhq_journey_session:${capture.journeyId}`;
10090
+ const clientSessionId = persisted && persisted.queue.length > 0 ? persisted.clientSessionId : session ? getStoredId(session, sessionKey, "fhqs") : randomId("fhqs");
10091
+ try {
10092
+ session == null ? void 0 : session.setItem(sessionKey, clientSessionId);
10093
+ } catch (e) {
10094
+ }
9990
10095
  const runtime = {
9991
10096
  journeyId: capture.journeyId,
9992
10097
  visitorId,
@@ -9994,51 +10099,46 @@ function useJourneyCapture(params) {
9994
10099
  queueKey
9995
10100
  };
9996
10101
  runtimeRef.current = runtime;
9997
- queueRef.current = readJson(
9998
- sessionStorage,
9999
- queueKey,
10000
- []
10001
- );
10102
+ queueRef.current = (_b = persisted == null ? void 0 : persisted.queue) != null ? _b : [];
10002
10103
  return runtime;
10003
10104
  }, []);
10004
- const flush = React.useCallback(async () => {
10005
- var _a, _b, _c;
10105
+ const flush = React.useCallback(async (options = {}) => {
10106
+ var _a, _b;
10006
10107
  const capture = captureRef.current;
10007
- if (!capture || !configRef.current || flushingRef.current) return;
10108
+ if (!capture || !configRef.current) return;
10109
+ if (!options.preferBeacon && flushingRef.current) return;
10008
10110
  const runtime = ensureRuntime(capture);
10009
10111
  if (!runtime || queueRef.current.length === 0) return;
10010
- flushingRef.current = true;
10112
+ queueRef.current = pruneQueuedEvents(queueRef.current);
10011
10113
  const batchSize = Math.max(1, (_a = capture.batchSize) != null ? _a : 10);
10012
10114
  const maxRetries = Math.max(1, (_b = capture.maxRetries) != null ? _b : 3);
10013
- const batch = queueRef.current.slice(0, batchSize);
10014
- const baseUrl = (_c = capture.baseUrl) != null ? _c : "https://getfounderhq.com";
10115
+ const batch = queueRef.current.slice(
10116
+ 0,
10117
+ options.preferBeacon ? Math.min(batchSize, 5) : batchSize
10118
+ );
10119
+ if (batch.length === 0) {
10120
+ persistQueue();
10121
+ return;
10122
+ }
10123
+ if (options.preferBeacon) {
10124
+ await sendCaptureBatch(capture, runtime, batch, options);
10125
+ persistQueue();
10126
+ return;
10127
+ }
10128
+ flushingRef.current = true;
10015
10129
  try {
10016
- const response = await fetch(
10017
- `${baseUrl}/api/v1/journeys/${encodeURIComponent(
10018
- capture.journeyId
10019
- )}/capture`,
10020
- {
10021
- method: "POST",
10022
- headers: {
10023
- Authorization: `Bearer ${capture.apiKey}`,
10024
- "Content-Type": "application/json"
10025
- },
10026
- body: JSON.stringify({
10027
- clientSessionId: runtime.clientSessionId,
10028
- visitorId: runtime.visitorId,
10029
- context: captureContext(capture, runtime),
10030
- events: batch.map((item) => item.event)
10031
- }),
10032
- keepalive: batch.length <= 5
10033
- }
10034
- );
10035
- if (!response.ok) throw new Error(`Capture failed: ${response.status}`);
10036
- queueRef.current = queueRef.current.slice(batch.length);
10037
- } catch (e) {
10038
- const failedIds = new Set(batch.map((item) => item.event.id));
10039
- queueRef.current = queueRef.current.map(
10040
- (item) => failedIds.has(item.event.id) ? __spreadProps(__spreadValues({}, item), { attempts: item.attempts + 1 }) : item
10041
- ).filter((item) => item.attempts < maxRetries);
10130
+ const result = await sendCaptureBatch(capture, runtime, batch);
10131
+ if (result === "acknowledged") {
10132
+ const sentIds = new Set(batch.map((item) => item.event.id));
10133
+ queueRef.current = queueRef.current.filter(
10134
+ (item) => !sentIds.has(item.event.id)
10135
+ );
10136
+ } else {
10137
+ const failedIds = new Set(batch.map((item) => item.event.id));
10138
+ queueRef.current = queueRef.current.map(
10139
+ (item) => failedIds.has(item.event.id) ? __spreadProps(__spreadValues({}, item), { attempts: item.attempts + 1 }) : item
10140
+ ).filter((item) => item.attempts < maxRetries);
10141
+ }
10042
10142
  } finally {
10043
10143
  persistQueue();
10044
10144
  flushingRef.current = false;
@@ -10054,15 +10154,18 @@ function useJourneyCapture(params) {
10054
10154
  Math.max(1e3, (_a = capture.flushIntervalMs) != null ? _a : 3e3)
10055
10155
  );
10056
10156
  const handleVisibility = () => {
10057
- if (document.visibilityState === "hidden") void flush();
10157
+ if (document.visibilityState === "hidden") {
10158
+ void flush({ preferBeacon: true });
10159
+ }
10058
10160
  };
10059
- window.addEventListener("beforeunload", persistQueue);
10161
+ const handlePageHide = () => void flush({ preferBeacon: true });
10060
10162
  document.addEventListener("visibilitychange", handleVisibility);
10163
+ window.addEventListener("pagehide", handlePageHide);
10061
10164
  void flush();
10062
10165
  return () => {
10063
10166
  window.clearInterval(interval);
10064
- window.removeEventListener("beforeunload", persistQueue);
10065
10167
  document.removeEventListener("visibilitychange", handleVisibility);
10168
+ window.removeEventListener("pagehide", handlePageHide);
10066
10169
  persistQueue();
10067
10170
  };
10068
10171
  }, [ensureRuntime, flush, persistQueue, params.capture]);
@@ -10078,8 +10181,12 @@ function useJourneyCapture(params) {
10078
10181
  sequenceRef.current += 1;
10079
10182
  queueRef.current.push({
10080
10183
  event: toCaptureEvent(config, event, sequenceRef.current, runtime),
10081
- attempts: 0
10184
+ attempts: 0,
10185
+ createdAt: Date.now()
10082
10186
  });
10187
+ queueRef.current = pruneQueuedEvents(queueRef.current).slice(
10188
+ -MAX_QUEUED_CAPTURE_EVENTS
10189
+ );
10083
10190
  persistQueue();
10084
10191
  const batchSize = Math.max(1, (_b = capture.batchSize) != null ? _b : 10);
10085
10192
  if (event.type === "complete" || queueRef.current.length >= batchSize) {
package/dist/index.js CHANGED
@@ -9668,20 +9668,15 @@ function JourneyShell({ className, theme } = {}) {
9668
9668
  ] });
9669
9669
  }
9670
9670
  var JOURNEY_LIBRARY_NAME = "@founderhq/journeys";
9671
- var JOURNEY_LIBRARY_VERSION = "0.4.1";
9671
+ var JOURNEY_LIBRARY_VERSION = "0.4.2";
9672
+ var DEFAULT_QUEUE_TTL_MS = 24 * 60 * 60 * 1e3;
9673
+ var MAX_QUEUED_CAPTURE_EVENTS = 100;
9674
+ var MAX_BEACON_BYTES = 60 * 1024;
9672
9675
  function randomId(prefix) {
9673
9676
  const cryptoRef = globalThis.crypto;
9674
9677
  if (cryptoRef == null ? void 0 : cryptoRef.randomUUID) return `${prefix}_${cryptoRef.randomUUID()}`;
9675
9678
  return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
9676
9679
  }
9677
- function readJson(storage, key, fallback) {
9678
- try {
9679
- const raw = storage.getItem(key);
9680
- return raw ? JSON.parse(raw) : fallback;
9681
- } catch (e) {
9682
- return fallback;
9683
- }
9684
- }
9685
9680
  function getStoredId(storage, key, prefix) {
9686
9681
  try {
9687
9682
  const existing = storage.getItem(key);
@@ -9693,6 +9688,64 @@ function getStoredId(storage, key, prefix) {
9693
9688
  return randomId(prefix);
9694
9689
  }
9695
9690
  }
9691
+ function safeBrowserStorage(name) {
9692
+ var _a;
9693
+ if (typeof window === "undefined") return null;
9694
+ try {
9695
+ return (_a = window[name]) != null ? _a : null;
9696
+ } catch (e) {
9697
+ return null;
9698
+ }
9699
+ }
9700
+ function isQueuedCaptureEvent(value) {
9701
+ if (!isRecord(value)) return false;
9702
+ const event = value.event;
9703
+ return isRecord(event) && typeof event.id === "string" && typeof event.type === "string" && typeof event.occurredAt === "string" && typeof value.attempts === "number" && typeof value.createdAt === "number";
9704
+ }
9705
+ function pruneQueuedEvents(queue, now = Date.now(), ttlMs = DEFAULT_QUEUE_TTL_MS) {
9706
+ const cutoff = now - ttlMs;
9707
+ return queue.filter((item) => item.createdAt >= cutoff);
9708
+ }
9709
+ function readPersistedCaptureQueue(storage, key) {
9710
+ if (!storage) return null;
9711
+ try {
9712
+ const raw = storage.getItem(key);
9713
+ if (!raw) return null;
9714
+ const parsed = JSON.parse(raw);
9715
+ if (typeof parsed.visitorId !== "string" || typeof parsed.clientSessionId !== "string") {
9716
+ return null;
9717
+ }
9718
+ const queue = Array.isArray(parsed.queue) ? pruneQueuedEvents(parsed.queue.filter(isQueuedCaptureEvent)) : [];
9719
+ return {
9720
+ visitorId: parsed.visitorId,
9721
+ clientSessionId: parsed.clientSessionId,
9722
+ updatedAt: typeof parsed.updatedAt === "number" ? parsed.updatedAt : Date.now(),
9723
+ queue
9724
+ };
9725
+ } catch (e) {
9726
+ return null;
9727
+ }
9728
+ }
9729
+ function persistCaptureQueue(storage, runtime, queue) {
9730
+ if (!storage) return;
9731
+ try {
9732
+ const pruned = pruneQueuedEvents(queue).slice(-MAX_QUEUED_CAPTURE_EVENTS);
9733
+ if (pruned.length === 0) {
9734
+ storage.removeItem(runtime.queueKey);
9735
+ return;
9736
+ }
9737
+ storage.setItem(
9738
+ runtime.queueKey,
9739
+ JSON.stringify({
9740
+ visitorId: runtime.visitorId,
9741
+ clientSessionId: runtime.clientSessionId,
9742
+ updatedAt: Date.now(),
9743
+ queue: pruned
9744
+ })
9745
+ );
9746
+ } catch (e) {
9747
+ }
9748
+ }
9696
9749
  function isRecord(value) {
9697
9750
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
9698
9751
  }
@@ -9793,6 +9846,54 @@ function captureContext(capture, runtime) {
9793
9846
  screen: screenRef ? { width: screenRef.width, height: screenRef.height } : void 0
9794
9847
  }, customContext));
9795
9848
  }
9849
+ function captureUrl(capture) {
9850
+ var _a;
9851
+ const baseUrl = (_a = capture.baseUrl) != null ? _a : "https://getfounderhq.com";
9852
+ return `${baseUrl}/api/v1/journeys/${encodeURIComponent(
9853
+ capture.journeyId
9854
+ )}/capture`;
9855
+ }
9856
+ function byteLength(value) {
9857
+ try {
9858
+ return new TextEncoder().encode(value).byteLength;
9859
+ } catch (e) {
9860
+ return value.length;
9861
+ }
9862
+ }
9863
+ function safeSendBeacon() {
9864
+ if (typeof navigator === "undefined") return null;
9865
+ const beacon = navigator.sendBeacon;
9866
+ return typeof beacon === "function" ? beacon.bind(navigator) : null;
9867
+ }
9868
+ async function sendCaptureBatch(capture, runtime, batch, options = {}) {
9869
+ const body = JSON.stringify(__spreadProps(__spreadValues({}, options.preferBeacon ? { apiKey: capture.apiKey } : {}), {
9870
+ clientSessionId: runtime.clientSessionId,
9871
+ visitorId: runtime.visitorId,
9872
+ context: captureContext(capture, runtime),
9873
+ events: batch.map((item) => item.event)
9874
+ }));
9875
+ const url = captureUrl(capture);
9876
+ if (options.preferBeacon && byteLength(body) <= MAX_BEACON_BYTES) {
9877
+ const beacon = safeSendBeacon();
9878
+ if (beacon == null ? void 0 : beacon(url, body)) {
9879
+ return "queued";
9880
+ }
9881
+ }
9882
+ try {
9883
+ const response = await fetch(url, {
9884
+ method: "POST",
9885
+ headers: {
9886
+ Authorization: `Bearer ${capture.apiKey}`,
9887
+ "Content-Type": "application/json"
9888
+ },
9889
+ body,
9890
+ keepalive: batch.length <= 5 || options.preferBeacon === true
9891
+ });
9892
+ return response.ok ? "acknowledged" : "failed";
9893
+ } catch (e) {
9894
+ return "failed";
9895
+ }
9896
+ }
9796
9897
  function toJsonRecord(value) {
9797
9898
  return isRecord(value) ? value : {};
9798
9899
  }
@@ -9938,30 +10039,34 @@ function useJourneyCapture(params) {
9938
10039
  captureRef.current = params.capture;
9939
10040
  const persistQueue = useCallback(() => {
9940
10041
  const runtime = runtimeRef.current;
9941
- if (!runtime || typeof window === "undefined") return;
9942
- try {
9943
- sessionStorage.setItem(
9944
- runtime.queueKey,
9945
- JSON.stringify(queueRef.current)
9946
- );
9947
- } catch (e) {
9948
- }
10042
+ if (!runtime) return;
10043
+ persistCaptureQueue(
10044
+ safeBrowserStorage("localStorage"),
10045
+ runtime,
10046
+ queueRef.current
10047
+ );
9949
10048
  }, []);
9950
10049
  const ensureRuntime = useCallback((capture) => {
10050
+ var _a, _b;
9951
10051
  if (typeof window === "undefined") return null;
10052
+ const queueKey = `fhq_journey_queue:v2:${capture.apiKey.slice(
10053
+ 0,
10054
+ 16
10055
+ )}:${capture.journeyId}`;
9952
10056
  const existing = runtimeRef.current;
9953
- if ((existing == null ? void 0 : existing.journeyId) === capture.journeyId) return existing;
9954
- const visitorId = getStoredId(
9955
- localStorage,
9956
- "fhq_journey_visitor_id",
9957
- "fhqv"
9958
- );
9959
- const clientSessionId = getStoredId(
9960
- sessionStorage,
9961
- `fhq_journey_session:${capture.journeyId}`,
9962
- "fhqs"
9963
- );
9964
- const queueKey = `fhq_journey_queue:${capture.journeyId}:${clientSessionId}`;
10057
+ if ((existing == null ? void 0 : existing.journeyId) === capture.journeyId && existing.queueKey === queueKey) {
10058
+ return existing;
10059
+ }
10060
+ const local = safeBrowserStorage("localStorage");
10061
+ const session = safeBrowserStorage("sessionStorage");
10062
+ const persisted = readPersistedCaptureQueue(local, queueKey);
10063
+ const visitorId = (_a = persisted == null ? void 0 : persisted.visitorId) != null ? _a : local ? getStoredId(local, "fhq_journey_visitor_id", "fhqv") : randomId("fhqv");
10064
+ const sessionKey = `fhq_journey_session:${capture.journeyId}`;
10065
+ const clientSessionId = persisted && persisted.queue.length > 0 ? persisted.clientSessionId : session ? getStoredId(session, sessionKey, "fhqs") : randomId("fhqs");
10066
+ try {
10067
+ session == null ? void 0 : session.setItem(sessionKey, clientSessionId);
10068
+ } catch (e) {
10069
+ }
9965
10070
  const runtime = {
9966
10071
  journeyId: capture.journeyId,
9967
10072
  visitorId,
@@ -9969,51 +10074,46 @@ function useJourneyCapture(params) {
9969
10074
  queueKey
9970
10075
  };
9971
10076
  runtimeRef.current = runtime;
9972
- queueRef.current = readJson(
9973
- sessionStorage,
9974
- queueKey,
9975
- []
9976
- );
10077
+ queueRef.current = (_b = persisted == null ? void 0 : persisted.queue) != null ? _b : [];
9977
10078
  return runtime;
9978
10079
  }, []);
9979
- const flush = useCallback(async () => {
9980
- var _a, _b, _c;
10080
+ const flush = useCallback(async (options = {}) => {
10081
+ var _a, _b;
9981
10082
  const capture = captureRef.current;
9982
- if (!capture || !configRef.current || flushingRef.current) return;
10083
+ if (!capture || !configRef.current) return;
10084
+ if (!options.preferBeacon && flushingRef.current) return;
9983
10085
  const runtime = ensureRuntime(capture);
9984
10086
  if (!runtime || queueRef.current.length === 0) return;
9985
- flushingRef.current = true;
10087
+ queueRef.current = pruneQueuedEvents(queueRef.current);
9986
10088
  const batchSize = Math.max(1, (_a = capture.batchSize) != null ? _a : 10);
9987
10089
  const maxRetries = Math.max(1, (_b = capture.maxRetries) != null ? _b : 3);
9988
- const batch = queueRef.current.slice(0, batchSize);
9989
- const baseUrl = (_c = capture.baseUrl) != null ? _c : "https://getfounderhq.com";
10090
+ const batch = queueRef.current.slice(
10091
+ 0,
10092
+ options.preferBeacon ? Math.min(batchSize, 5) : batchSize
10093
+ );
10094
+ if (batch.length === 0) {
10095
+ persistQueue();
10096
+ return;
10097
+ }
10098
+ if (options.preferBeacon) {
10099
+ await sendCaptureBatch(capture, runtime, batch, options);
10100
+ persistQueue();
10101
+ return;
10102
+ }
10103
+ flushingRef.current = true;
9990
10104
  try {
9991
- const response = await fetch(
9992
- `${baseUrl}/api/v1/journeys/${encodeURIComponent(
9993
- capture.journeyId
9994
- )}/capture`,
9995
- {
9996
- method: "POST",
9997
- headers: {
9998
- Authorization: `Bearer ${capture.apiKey}`,
9999
- "Content-Type": "application/json"
10000
- },
10001
- body: JSON.stringify({
10002
- clientSessionId: runtime.clientSessionId,
10003
- visitorId: runtime.visitorId,
10004
- context: captureContext(capture, runtime),
10005
- events: batch.map((item) => item.event)
10006
- }),
10007
- keepalive: batch.length <= 5
10008
- }
10009
- );
10010
- if (!response.ok) throw new Error(`Capture failed: ${response.status}`);
10011
- queueRef.current = queueRef.current.slice(batch.length);
10012
- } catch (e) {
10013
- const failedIds = new Set(batch.map((item) => item.event.id));
10014
- queueRef.current = queueRef.current.map(
10015
- (item) => failedIds.has(item.event.id) ? __spreadProps(__spreadValues({}, item), { attempts: item.attempts + 1 }) : item
10016
- ).filter((item) => item.attempts < maxRetries);
10105
+ const result = await sendCaptureBatch(capture, runtime, batch);
10106
+ if (result === "acknowledged") {
10107
+ const sentIds = new Set(batch.map((item) => item.event.id));
10108
+ queueRef.current = queueRef.current.filter(
10109
+ (item) => !sentIds.has(item.event.id)
10110
+ );
10111
+ } else {
10112
+ const failedIds = new Set(batch.map((item) => item.event.id));
10113
+ queueRef.current = queueRef.current.map(
10114
+ (item) => failedIds.has(item.event.id) ? __spreadProps(__spreadValues({}, item), { attempts: item.attempts + 1 }) : item
10115
+ ).filter((item) => item.attempts < maxRetries);
10116
+ }
10017
10117
  } finally {
10018
10118
  persistQueue();
10019
10119
  flushingRef.current = false;
@@ -10029,15 +10129,18 @@ function useJourneyCapture(params) {
10029
10129
  Math.max(1e3, (_a = capture.flushIntervalMs) != null ? _a : 3e3)
10030
10130
  );
10031
10131
  const handleVisibility = () => {
10032
- if (document.visibilityState === "hidden") void flush();
10132
+ if (document.visibilityState === "hidden") {
10133
+ void flush({ preferBeacon: true });
10134
+ }
10033
10135
  };
10034
- window.addEventListener("beforeunload", persistQueue);
10136
+ const handlePageHide = () => void flush({ preferBeacon: true });
10035
10137
  document.addEventListener("visibilitychange", handleVisibility);
10138
+ window.addEventListener("pagehide", handlePageHide);
10036
10139
  void flush();
10037
10140
  return () => {
10038
10141
  window.clearInterval(interval);
10039
- window.removeEventListener("beforeunload", persistQueue);
10040
10142
  document.removeEventListener("visibilitychange", handleVisibility);
10143
+ window.removeEventListener("pagehide", handlePageHide);
10041
10144
  persistQueue();
10042
10145
  };
10043
10146
  }, [ensureRuntime, flush, persistQueue, params.capture]);
@@ -10053,8 +10156,12 @@ function useJourneyCapture(params) {
10053
10156
  sequenceRef.current += 1;
10054
10157
  queueRef.current.push({
10055
10158
  event: toCaptureEvent(config, event, sequenceRef.current, runtime),
10056
- attempts: 0
10159
+ attempts: 0,
10160
+ createdAt: Date.now()
10057
10161
  });
10162
+ queueRef.current = pruneQueuedEvents(queueRef.current).slice(
10163
+ -MAX_QUEUED_CAPTURE_EVENTS
10164
+ );
10058
10165
  persistQueue();
10059
10166
  const batchSize = Math.max(1, (_b = capture.batchSize) != null ? _b : 10);
10060
10167
  if (event.type === "complete" || queueRef.current.length >= batchSize) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@founderhq/journeys",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Config-driven interactive journey/questionnaire engine for React",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",