saltfish 0.3.71 → 0.3.74

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.
@@ -2983,7 +2983,6 @@ class PlaylistOrchestrator {
2983
2983
  });
2984
2984
  }
2985
2985
  }
2986
- this.managers.analyticsManager.trackPlaylistStart(playlistId);
2987
2986
  this.managers.cursorManager.resetFirstAnimation();
2988
2987
  log(`[PlaylistOrchestrator.startPlaylist] Using validated manifest path: ${manifestPathToLoad}`);
2989
2988
  await this.managers.playlistManager.load(manifestPathToLoad, { ...finalOptions, persistence: playlistPersistence });
@@ -3750,6 +3749,39 @@ function setupEventUpdater(eventManager) {
3750
3749
  let prevPreviousState = null;
3751
3750
  let prevIsMinimized = null;
3752
3751
  let prevStepId = null;
3752
+ let pendingStartEvents = { playlistData: null, stepData: null };
3753
+ let autoplayConfirmationTimer = null;
3754
+ const firePendingEvents = () => {
3755
+ if (autoplayConfirmationTimer) {
3756
+ clearTimeout(autoplayConfirmationTimer);
3757
+ autoplayConfirmationTimer = null;
3758
+ }
3759
+ if (pendingStartEvents.playlistData) {
3760
+ eventManager.trigger("playlistStarted", {
3761
+ timestamp: Date.now(),
3762
+ playlist: pendingStartEvents.playlistData
3763
+ });
3764
+ }
3765
+ if (pendingStartEvents.stepData) {
3766
+ eventManager.trigger("stepStarted", {
3767
+ timestamp: Date.now(),
3768
+ step: { id: pendingStartEvents.stepData.id, title: pendingStartEvents.stepData.title },
3769
+ playlist: { id: pendingStartEvents.stepData.playlistId, title: pendingStartEvents.stepData.playlistTitle }
3770
+ });
3771
+ }
3772
+ pendingStartEvents = { playlistData: null, stepData: null };
3773
+ };
3774
+ const startAutoplayConfirmationTimer = () => {
3775
+ if (autoplayConfirmationTimer) {
3776
+ clearTimeout(autoplayConfirmationTimer);
3777
+ }
3778
+ autoplayConfirmationTimer = setTimeout(() => {
3779
+ if (pendingStartEvents.playlistData || pendingStartEvents.stepData) {
3780
+ firePendingEvents();
3781
+ }
3782
+ autoplayConfirmationTimer = null;
3783
+ }, 500);
3784
+ };
3753
3785
  const unsubscribe = useSaltfishStore.subscribe(
3754
3786
  (state) => {
3755
3787
  var _a;
@@ -3774,30 +3806,55 @@ function setupEventUpdater(eventManager) {
3774
3806
  log(`EventUpdater: Processing state change from '${eventData.previousState}' to '${eventData.currentState}'`, {
3775
3807
  manifestId: (_a = state.manifest) == null ? void 0 : _a.id
3776
3808
  });
3777
- handleStateTransitionEvents(eventData, state, eventManager);
3809
+ if (eventData.previousState === "playing") {
3810
+ if (eventData.currentState === "autoplayBlocked" || eventData.currentState === "idleMode") {
3811
+ if (autoplayConfirmationTimer) {
3812
+ clearTimeout(autoplayConfirmationTimer);
3813
+ autoplayConfirmationTimer = null;
3814
+ }
3815
+ if (pendingStartEvents.playlistData || pendingStartEvents.stepData) {
3816
+ pendingStartEvents = { playlistData: null, stepData: null };
3817
+ }
3818
+ } else if (pendingStartEvents.playlistData || pendingStartEvents.stepData) {
3819
+ firePendingEvents();
3820
+ }
3821
+ }
3822
+ handleStateTransitionEvents(eventData, state, eventManager, pendingStartEvents);
3778
3823
  handleMinimizeEvents(eventData, state, eventManager, actualPrevMinimized);
3779
- handleStepEvents(eventData, state, eventManager, actualPrevStepId);
3824
+ handleStepEvents(eventData, state, eventManager, actualPrevStepId, pendingStartEvents);
3780
3825
  handleErrorEvents(eventData, state, eventManager);
3826
+ if ((pendingStartEvents.playlistData || pendingStartEvents.stepData) && !autoplayConfirmationTimer) {
3827
+ startAutoplayConfirmationTimer();
3828
+ }
3781
3829
  }
3782
3830
  );
3783
3831
  return unsubscribe;
3784
3832
  }
3785
- function handleStateTransitionEvents(eventData, store, eventManager) {
3833
+ function handleStateTransitionEvents(eventData, store, eventManager, pendingStartEvents) {
3786
3834
  const { prevPreviousState, previousState, currentState, currentStepId } = eventData;
3787
3835
  const isNormalPlaylistStart = currentState === "playing" && previousState === "paused" && prevPreviousState === "loading";
3788
- const isDelayedPlaylistStart = currentState === "playing" && (previousState === "autoplayBlocked" || previousState === "idleMode");
3789
- if ((isNormalPlaylistStart || isDelayedPlaylistStart) && store.manifest && currentStepId) {
3836
+ const isUserInitiatedPlaylistStart = currentState === "playing" && (previousState === "autoplayBlocked" || previousState === "idleMode");
3837
+ if (store.manifest && currentStepId) {
3790
3838
  const isStartingNode = currentStepId === store.manifest.startStep;
3791
3839
  if (isStartingNode) {
3792
- log(`EventUpdater: Triggering playlistStarted event for ${store.manifest.id} (starting node: ${currentStepId})`);
3793
- eventManager.trigger("playlistStarted", {
3794
- timestamp: Date.now(),
3795
- playlist: {
3840
+ if (isUserInitiatedPlaylistStart) {
3841
+ log(`EventUpdater: Triggering playlistStarted event for ${store.manifest.id} (user-initiated from ${previousState})`);
3842
+ eventManager.trigger("playlistStarted", {
3843
+ timestamp: Date.now(),
3844
+ playlist: {
3845
+ id: store.manifest.id,
3846
+ title: store.manifest.name
3847
+ }
3848
+ });
3849
+ return;
3850
+ } else if (isNormalPlaylistStart) {
3851
+ log(`EventUpdater: Deferring playlistStarted event for ${store.manifest.id} (pending autoplay confirmation)`);
3852
+ pendingStartEvents.playlistData = {
3796
3853
  id: store.manifest.id,
3797
3854
  title: store.manifest.name
3798
- }
3799
- });
3800
- return;
3855
+ };
3856
+ return;
3857
+ }
3801
3858
  }
3802
3859
  }
3803
3860
  if (previousState === "paused" && currentState === "playing") {
@@ -3831,7 +3888,7 @@ function handleMinimizeEvents(eventData, _store, eventManager, prevIsMinimized)
3831
3888
  });
3832
3889
  }
3833
3890
  }
3834
- function handleStepEvents(eventData, store, eventManager, prevStepId) {
3891
+ function handleStepEvents(eventData, store, eventManager, prevStepId, pendingStartEvents) {
3835
3892
  var _a, _b, _c;
3836
3893
  const { prevPreviousState, previousState, currentStepId, currentState } = eventData;
3837
3894
  const currentStep = store.currentStepId ? (((_a = store.manifest) == null ? void 0 : _a.steps) || []).find((s) => s.id === store.currentStepId) : null;
@@ -3865,26 +3922,38 @@ function handleStepEvents(eventData, store, eventManager, prevStepId) {
3865
3922
  }
3866
3923
  }
3867
3924
  const isStepChange = currentStepId !== prevStepId && currentState === "playing";
3868
- const isAutoplayOrIdleModeTransition = (previousState === "autoplayBlocked" || previousState === "idleMode") && currentState === "playing" && prevStepId === currentStepId;
3869
- const isFirstStepOfPlaylist = prevPreviousState === "loading" && previousState === "paused" && currentState === "playing" && currentStepId;
3870
- const shouldTriggerStepStarted = currentStep && store.manifest && (isStepChange || isAutoplayOrIdleModeTransition || isFirstStepOfPlaylist);
3925
+ const isUserInitiatedStepStart = (previousState === "autoplayBlocked" || previousState === "idleMode") && currentState === "playing" && prevStepId === currentStepId;
3926
+ const isFirstPlayOfPlaylist = prevPreviousState === "loading" && previousState === "paused" && currentState === "playing" && currentStepId;
3927
+ const isStartingNode = store.manifest && currentStepId === store.manifest.startStep;
3928
+ const isFirstStepAutoplayAttempt = isStartingNode && isFirstPlayOfPlaylist;
3929
+ const isNonStartingNodeFirstPlay = !isStartingNode && isFirstPlayOfPlaylist;
3871
3930
  log("EventUpdater.handleStepEvents: Step started check", {
3872
3931
  currentStep: currentStep == null ? void 0 : currentStep.id,
3873
3932
  hasManifest: !!store.manifest
3874
3933
  });
3875
- if (shouldTriggerStepStarted) {
3876
- log(`EventUpdater: Triggering stepStarted for ${currentStep.id}`);
3877
- eventManager.trigger("stepStarted", {
3878
- timestamp: Date.now(),
3879
- step: {
3934
+ if (currentStep && store.manifest) {
3935
+ if (isStepChange || isUserInitiatedStepStart || isNonStartingNodeFirstPlay) {
3936
+ log(`EventUpdater: Triggering stepStarted for ${currentStep.id}`);
3937
+ eventManager.trigger("stepStarted", {
3938
+ timestamp: Date.now(),
3939
+ step: {
3940
+ id: currentStep.id,
3941
+ title: currentStep.title || currentStep.id
3942
+ },
3943
+ playlist: {
3944
+ id: store.manifest.id,
3945
+ title: store.manifest.name
3946
+ }
3947
+ });
3948
+ } else if (isFirstStepAutoplayAttempt) {
3949
+ log(`EventUpdater: Deferring stepStarted for ${currentStep.id} (pending autoplay confirmation)`);
3950
+ pendingStartEvents.stepData = {
3880
3951
  id: currentStep.id,
3881
- title: currentStep.title || currentStep.id
3882
- },
3883
- playlist: {
3884
- id: store.manifest.id,
3885
- title: store.manifest.name
3886
- }
3887
- });
3952
+ title: currentStep.title || currentStep.id,
3953
+ playlistId: store.manifest.id,
3954
+ playlistTitle: store.manifest.name
3955
+ };
3956
+ }
3888
3957
  }
3889
3958
  }
3890
3959
  function handleErrorEvents(eventData, store, eventManager) {
@@ -6956,6 +7025,32 @@ function isElementValid(element, expectedElement, expectedSize, config = DEFAULT
6956
7025
  }
6957
7026
  return true;
6958
7027
  }
7028
+ const RRWEB_SNAPSHOT_URL = "https://storage.saltfish.ai/libs/rrweb-snapshot-2.0.0-alpha.18.js";
7029
+ async function captureDOMSnapshot() {
7030
+ try {
7031
+ const rrwebSnapshot = await import(
7032
+ /* webpackIgnore: true */
7033
+ RRWEB_SNAPSHOT_URL
7034
+ );
7035
+ const domSnapshot = rrwebSnapshot.snapshot(document);
7036
+ return domSnapshot;
7037
+ } catch (error2) {
7038
+ console.warn("Failed to capture DOM snapshot:", error2);
7039
+ return null;
7040
+ }
7041
+ }
7042
+ async function captureDOMSnapshotAsString() {
7043
+ const snapshot = await captureDOMSnapshot();
7044
+ if (!snapshot) {
7045
+ return null;
7046
+ }
7047
+ try {
7048
+ return JSON.stringify(snapshot);
7049
+ } catch (error2) {
7050
+ console.warn("Failed to serialize DOM snapshot:", error2);
7051
+ return null;
7052
+ }
7053
+ }
6959
7054
  const API_URL = "https://player.saltfish.ai/element-errors";
6960
7055
  function determineFailureReason(selector, expectedElement, expectedSize) {
6961
7056
  const elements = document.querySelectorAll(selector);
@@ -6978,21 +7073,29 @@ function determineFailureReason(selector, expectedElement, expectedSize) {
6978
7073
  }
6979
7074
  function reportElementError(playlistId, stepId, selector, expectedElement, expectedSize) {
6980
7075
  const failureReason = determineFailureReason(selector, expectedElement, expectedSize);
6981
- const payload = {
6982
- playlistId,
6983
- stepId,
6984
- failureReason,
6985
- selector
6986
- };
6987
- fetch(API_URL, {
6988
- method: "POST",
6989
- headers: {
6990
- "Content-Type": "application/json"
6991
- },
6992
- body: JSON.stringify(payload)
6993
- }).catch((error2) => {
6994
- console.warn("Failed to report element error:", error2);
6995
- });
7076
+ (async () => {
7077
+ try {
7078
+ const payload = {
7079
+ playlistId,
7080
+ stepId,
7081
+ failureReason,
7082
+ selector
7083
+ };
7084
+ const domSnapshot = await captureDOMSnapshotAsString();
7085
+ if (domSnapshot) {
7086
+ payload.domSnapshot = domSnapshot;
7087
+ }
7088
+ await fetch(API_URL, {
7089
+ method: "POST",
7090
+ headers: {
7091
+ "Content-Type": "application/json"
7092
+ },
7093
+ body: JSON.stringify(payload)
7094
+ });
7095
+ } catch (error2) {
7096
+ console.warn("Failed to report element error:", error2);
7097
+ }
7098
+ })();
6996
7099
  }
6997
7100
  class CursorManager {
6998
7101
  constructor() {
@@ -8960,7 +9063,6 @@ class EventSubscriberManager {
8960
9063
  }
8961
9064
  }
8962
9065
  class AnalyticsManager extends EventSubscriberManager {
8963
- // Default to enabled
8964
9066
  /**
8965
9067
  * Creates a new AnalyticsManager
8966
9068
  * @param eventManager - Optional event manager to subscribe to events
@@ -8974,6 +9076,8 @@ class AnalyticsManager extends EventSubscriberManager {
8974
9076
  __publicField(this, "flushInterval", null);
8975
9077
  __publicField(this, "sessionId", null);
8976
9078
  __publicField(this, "analyticsEnabled", true);
9079
+ // Default to enabled
9080
+ __publicField(this, "boundVisibilityHandler", null);
8977
9081
  }
8978
9082
  /**
8979
9083
  * Subscribe to relevant player events for analytics tracking
@@ -9060,6 +9164,10 @@ class AnalyticsManager extends EventSubscriberManager {
9060
9164
  });
9061
9165
  }
9062
9166
  });
9167
+ this.eventManager.on("playlistStarted", (event) => {
9168
+ log(`AnalyticsManager: Playlist started event received - ${event.playlist.id}`);
9169
+ this.trackPlaylistStart(event.playlist.id);
9170
+ });
9063
9171
  }
9064
9172
  /**
9065
9173
  * Initializes the analytics manager
@@ -9076,6 +9184,8 @@ class AnalyticsManager extends EventSubscriberManager {
9076
9184
  this.flushInterval = window.setInterval(() => {
9077
9185
  this.flushEvents();
9078
9186
  }, ANALYTICS.FLUSH_INTERVAL_MS);
9187
+ this.boundVisibilityHandler = this.handleVisibilityChange.bind(this);
9188
+ document.addEventListener("visibilitychange", this.boundVisibilityHandler);
9079
9189
  }
9080
9190
  }
9081
9191
  /**
@@ -9272,6 +9382,45 @@ class AnalyticsManager extends EventSubscriberManager {
9272
9382
  this.isSending = false;
9273
9383
  }
9274
9384
  }
9385
+ /**
9386
+ * Handles visibility change events to flush analytics when page is hidden
9387
+ * Uses sendBeacon for reliable delivery during page unload
9388
+ */
9389
+ handleVisibilityChange() {
9390
+ if (document.visibilityState === "hidden") {
9391
+ this.flushWithBeacon();
9392
+ }
9393
+ }
9394
+ /**
9395
+ * Flushes events using sendBeacon API for reliable delivery during page unload
9396
+ * sendBeacon is fire-and-forget and survives page navigation/close
9397
+ */
9398
+ flushWithBeacon() {
9399
+ if (this.eventQueue.length === 0 || !this.config || !this.analyticsEnabled) {
9400
+ return;
9401
+ }
9402
+ const events = [...this.eventQueue];
9403
+ this.eventQueue = [];
9404
+ const payload = {
9405
+ token: this.config.token,
9406
+ sessionId: this.sessionId,
9407
+ user: this.user,
9408
+ events
9409
+ };
9410
+ try {
9411
+ fetch("https://player.saltfish.ai/analytics", {
9412
+ method: "POST",
9413
+ headers: {
9414
+ "Content-Type": "application/json"
9415
+ },
9416
+ body: JSON.stringify(payload),
9417
+ keepalive: true
9418
+ });
9419
+ } catch (error2) {
9420
+ console.warn("AnalyticsManager: fetch keepalive failed, events returned to queue");
9421
+ this.eventQueue = [...events, ...this.eventQueue];
9422
+ }
9423
+ }
9275
9424
  /**
9276
9425
  * Cleans up resources used by the analytics manager
9277
9426
  */
@@ -9283,6 +9432,10 @@ class AnalyticsManager extends EventSubscriberManager {
9283
9432
  clearInterval(this.flushInterval);
9284
9433
  this.flushInterval = null;
9285
9434
  }
9435
+ if (this.boundVisibilityHandler) {
9436
+ document.removeEventListener("visibilitychange", this.boundVisibilityHandler);
9437
+ this.boundVisibilityHandler = null;
9438
+ }
9286
9439
  if (this.eventManager) {
9287
9440
  this.eventManager = null;
9288
9441
  }
@@ -12590,7 +12743,7 @@ const SaltfishPlayer$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.de
12590
12743
  __proto__: null,
12591
12744
  SaltfishPlayer
12592
12745
  }, Symbol.toStringTag, { value: "Module" }));
12593
- const version = "0.3.71";
12746
+ const version = "0.3.74";
12594
12747
  const packageJson = {
12595
12748
  version
12596
12749
  };