@posthog/browser-common 0.10.4 → 0.10.6

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/client.d.ts CHANGED
@@ -108,8 +108,14 @@ export interface Client {
108
108
  readonly isOptedOut: boolean;
109
109
  /** Whether the host currently permits data capture. */
110
110
  readonly canCapture: boolean;
111
- /** Synchronously admits an analytics event to the client's normal pipeline. */
112
- capture(event: string, properties?: Properties | null, options?: CaptureOptions): void;
111
+ /** Whether the client rate limit drops an event captured now. Reading it must not use a token. */
112
+ readonly isRateLimited?: boolean;
113
+ /**
114
+ * Synchronously admits an analytics event to the client's normal pipeline.
115
+ * Returns `false` when the client rate limit dropped the event, so the caller can send it again later.
116
+ * An event that is dropped for another reason, for example by `before_send`, returns `true`.
117
+ */
118
+ capture(event: string, properties?: Properties | null, options?: CaptureOptions): boolean;
113
119
  /** Registers a synchronous producer of properties merged into every captured event. */
114
120
  registerDynamicEventProperties(producer: () => Record<string, unknown>): Disposable;
115
121
  /** Returns the extension registered under a typed stable name, or `undefined` when it is not installed. */
package/dist/config.js CHANGED
@@ -27,7 +27,7 @@ var __webpack_require__ = {};
27
27
  })();
28
28
  var __webpack_exports__ = {};
29
29
  __webpack_require__.r(__webpack_exports__);
30
- const packageVersion = "0.10.4";
30
+ const packageVersion = "0.10.6";
31
31
  const Config = {
32
32
  DEBUG: false,
33
33
  LIB_VERSION: packageVersion,
package/dist/config.mjs CHANGED
@@ -1,4 +1,4 @@
1
- const packageVersion = "0.10.4";
1
+ const packageVersion = "0.10.6";
2
2
  const Config = {
3
3
  DEBUG: false,
4
4
  LIB_VERSION: packageVersion,
@@ -76,6 +76,8 @@ export declare class PostHogFeatureFlags implements Extension {
76
76
  private _additionalReloadRequested;
77
77
  private _reloadDebouncer;
78
78
  private _firingFeatureFlagsCallbacks;
79
+ private _flagCallsPendingCapture;
80
+ private _sendingFlagCallsPendingCapture;
79
81
  private _flagsLoadedFromRemote;
80
82
  private _staleCacheRefreshTriggered;
81
83
  private _consecutiveStatusZeroFailures;
@@ -153,6 +155,8 @@ export declare class PostHogFeatureFlags implements Extension {
153
155
  setAnonymousDistinctId(anon_distinct_id: string | undefined): void;
154
156
  setReloadingPaused(isPaused: boolean): void;
155
157
  resetFlagCallReported(): void;
158
+ /** Sends `$feature_flag_called` for the flags read while capture was off, once the client can capture. */
159
+ sendFlagCallsPendingCapture(): void;
156
160
  _callFlagsEndpoint(options?: {
157
161
  disableFlags?: boolean;
158
162
  }): void;
@@ -224,7 +228,11 @@ export declare class PostHogFeatureFlags implements Extension {
224
228
  * @returns {FeatureFlagResult | undefined} The feature flag result including key, enabled, variant, and payload.
225
229
  */
226
230
  getFeatureFlagResult(key: string, options?: FeatureFlagOptions): FeatureFlagResult | undefined;
231
+ /** Returns `false` when the client rate limit dropped the event. */
227
232
  private _captureFeatureFlagCalled;
233
+ private _holdFlagCall;
234
+ private _flagCallContext;
235
+ private _unmarkFlagCallReported;
228
236
  getRemoteConfigPayload(key: string, callback: RemoteConfigFeatureFlagCallback): void;
229
237
  private _getRemoteConfigPayload;
230
238
  /**
@@ -190,6 +190,11 @@ const QuotaLimitedResource = {
190
190
  FeatureFlags: 'feature_flags',
191
191
  Recordings: 'recordings'
192
192
  };
193
+ const haveSameGroups = (a, b)=>{
194
+ const keys = Object.keys(a);
195
+ return keys.length === Object.keys(b).length && keys.every((key)=>a[key] === b[key]);
196
+ };
197
+ const isSameFlagCallContext = (a, b)=>a.distinctId === b.distinctId && a.sessionId === b.sessionId && haveSameGroups(a.groups, b.groups);
193
198
  class PostHogFeatureFlags {
194
199
  constructor(configSource){
195
200
  this.name = 'featureFlags';
@@ -207,6 +212,8 @@ class PostHogFeatureFlags {
207
212
  this._reloadingDisabled = false;
208
213
  this._additionalReloadRequested = false;
209
214
  this._firingFeatureFlagsCallbacks = false;
215
+ this._flagCallsPendingCapture = new Map();
216
+ this._sendingFlagCallsPendingCapture = false;
210
217
  this._flagsLoadedFromRemote = false;
211
218
  this._staleCacheRefreshTriggered = false;
212
219
  this._consecutiveStatusZeroFailures = 0;
@@ -650,6 +657,27 @@ class PostHogFeatureFlags {
650
657
  }
651
658
  resetFlagCallReported() {
652
659
  this._remove(external_constants_js_namespaceObject.FLAG_CALL_REPORTED);
660
+ this._flagCallsPendingCapture = new Map();
661
+ }
662
+ sendFlagCallsPendingCapture() {
663
+ if (0 === this._flagCallsPendingCapture.size || this._sendingFlagCallsPendingCapture || !this._client?.canCapture || this._client.isRateLimited) return;
664
+ this._sendingFlagCallsPendingCapture = true;
665
+ try {
666
+ const flagVariants = this.getFlagVariants();
667
+ for (const key of Array.from(this._flagCallsPendingCapture.keys())){
668
+ const pending = this._flagCallsPendingCapture.get(key);
669
+ if (pending) {
670
+ if (pending.value !== String(flagVariants[key]) || !isSameFlagCallContext(pending.context, this._flagCallContext())) {
671
+ this._flagCallsPendingCapture.delete(key);
672
+ continue;
673
+ }
674
+ this.getFeatureFlagResult(key);
675
+ if (this._flagCallsPendingCapture.has(key)) break;
676
+ }
677
+ }
678
+ } finally{
679
+ this._sendingFlagCallsPendingCapture = false;
680
+ }
653
681
  }
654
682
  _callFlagsEndpoint(options) {
655
683
  this._clearDebouncer();
@@ -812,12 +840,17 @@ class PostHogFeatureFlags {
812
840
  [external_constants_js_namespaceObject.FLAG_CALL_REPORTED_SESSION_ID]: sessionIdToPersist
813
841
  });
814
842
  } else {
815
- const canCapture = this._client?.canCapture;
816
- if (!canCapture && Object.keys(flagCallReported).length > 0) {
817
- flagCallReported = {};
818
- this._set({
819
- [external_constants_js_namespaceObject.FLAG_CALL_REPORTED]: flagCallReported
820
- });
843
+ const captureOff = !this._client?.canCapture;
844
+ const canCapture = !captureOff && !this._client?.isRateLimited;
845
+ if (canCapture) this._flagCallsPendingCapture.delete(key);
846
+ else {
847
+ if (captureOff && Object.keys(flagCallReported).length > 0) {
848
+ flagCallReported = {};
849
+ this._set({
850
+ [external_constants_js_namespaceObject.FLAG_CALL_REPORTED]: flagCallReported
851
+ });
852
+ }
853
+ if (!flagCallReported[key]?.includes(flagReportValue)) this._holdFlagCall(key, flagReportValue);
821
854
  }
822
855
  if (canCapture && !flagCallReported[key]?.includes(flagReportValue)) {
823
856
  if ((0, core_namespaceObject.isArray)(flagCallReported[key])) flagCallReported[key].push(flagReportValue);
@@ -853,7 +886,10 @@ class PostHogFeatureFlags {
853
886
  if (!(0, core_namespaceObject.isUndefined)(flagDetails?.original_variant) || !(0, core_namespaceObject.isUndefined)(flagDetails?.original_enabled)) properties.$feature_flag_original_response = (0, core_namespaceObject.isUndefined)(flagDetails.original_variant) ? flagDetails.original_enabled : flagDetails.original_variant;
854
887
  if (flagDetails?.metadata?.original_payload) properties.$feature_flag_original_payload = flagDetails?.metadata?.original_payload;
855
888
  if (errors.length) properties.$feature_flag_error = errors.join(',');
856
- this._captureFeatureFlagCalled(properties);
889
+ if (!this._captureFeatureFlagCalled(properties)) {
890
+ this._unmarkFlagCallReported(key, flagReportValue);
891
+ this._holdFlagCall(key, flagReportValue);
892
+ }
857
893
  } else if (sessionIdToPersist) this._set({
858
894
  [external_constants_js_namespaceObject.FLAG_CALL_REPORTED]: flagCallReported,
859
895
  [external_constants_js_namespaceObject.FLAG_CALL_REPORTED_SESSION_ID]: sessionIdToPersist
@@ -869,11 +905,36 @@ class PostHogFeatureFlags {
869
905
  }
870
906
  _captureFeatureFlagCalled(properties) {
871
907
  try {
872
- this._client?.capture('$feature_flag_called', properties);
908
+ return this._client?.capture('$feature_flag_called', properties) !== false;
873
909
  } catch (error) {
874
910
  this._logger.error('Failed to capture feature flag call', error);
911
+ return true;
875
912
  }
876
913
  }
914
+ _holdFlagCall(key, value) {
915
+ this._flagCallsPendingCapture.set(key, {
916
+ value,
917
+ context: this._flagCallContext()
918
+ });
919
+ }
920
+ _flagCallContext() {
921
+ return {
922
+ distinctId: this._client?.distinctId,
923
+ groups: {
924
+ ...this._client?.groups
925
+ },
926
+ sessionId: this._config.deduplicateCallsPerSession ? this._client?.session?.sessionId : void 0
927
+ };
928
+ }
929
+ _unmarkFlagCallReported(key, flagReportValue) {
930
+ const flagCallReported = this._prop(external_constants_js_namespaceObject.FLAG_CALL_REPORTED) || {};
931
+ const reportedValues = flagCallReported[key];
932
+ if (!(0, core_namespaceObject.isArray)(reportedValues) || !reportedValues.includes(flagReportValue)) return;
933
+ flagCallReported[key] = reportedValues.filter((value)=>value !== flagReportValue);
934
+ this._set({
935
+ [external_constants_js_namespaceObject.FLAG_CALL_REPORTED]: flagCallReported
936
+ });
937
+ }
877
938
  getRemoteConfigPayload(key, callback) {
878
939
  this._getRemoteConfigPayload(key, callback);
879
940
  }
@@ -1215,6 +1276,7 @@ class PostHogFeatureFlags {
1215
1276
  this._clearDebouncer();
1216
1277
  this._override_warning = false;
1217
1278
  this._consecutiveStatusZeroFailures = 0;
1279
+ this._flagCallsPendingCapture = new Map();
1218
1280
  }
1219
1281
  }
1220
1282
  exports.FeatureFlagError = __webpack_exports__.FeatureFlagError;
@@ -154,6 +154,11 @@ const QuotaLimitedResource = {
154
154
  FeatureFlags: 'feature_flags',
155
155
  Recordings: 'recordings'
156
156
  };
157
+ const haveSameGroups = (a, b)=>{
158
+ const keys = Object.keys(a);
159
+ return keys.length === Object.keys(b).length && keys.every((key)=>a[key] === b[key]);
160
+ };
161
+ const isSameFlagCallContext = (a, b)=>a.distinctId === b.distinctId && a.sessionId === b.sessionId && haveSameGroups(a.groups, b.groups);
157
162
  class PostHogFeatureFlags {
158
163
  constructor(configSource){
159
164
  this.name = 'featureFlags';
@@ -171,6 +176,8 @@ class PostHogFeatureFlags {
171
176
  this._reloadingDisabled = false;
172
177
  this._additionalReloadRequested = false;
173
178
  this._firingFeatureFlagsCallbacks = false;
179
+ this._flagCallsPendingCapture = new Map();
180
+ this._sendingFlagCallsPendingCapture = false;
174
181
  this._flagsLoadedFromRemote = false;
175
182
  this._staleCacheRefreshTriggered = false;
176
183
  this._consecutiveStatusZeroFailures = 0;
@@ -614,6 +621,27 @@ class PostHogFeatureFlags {
614
621
  }
615
622
  resetFlagCallReported() {
616
623
  this._remove(FLAG_CALL_REPORTED);
624
+ this._flagCallsPendingCapture = new Map();
625
+ }
626
+ sendFlagCallsPendingCapture() {
627
+ if (0 === this._flagCallsPendingCapture.size || this._sendingFlagCallsPendingCapture || !this._client?.canCapture || this._client.isRateLimited) return;
628
+ this._sendingFlagCallsPendingCapture = true;
629
+ try {
630
+ const flagVariants = this.getFlagVariants();
631
+ for (const key of Array.from(this._flagCallsPendingCapture.keys())){
632
+ const pending = this._flagCallsPendingCapture.get(key);
633
+ if (pending) {
634
+ if (pending.value !== String(flagVariants[key]) || !isSameFlagCallContext(pending.context, this._flagCallContext())) {
635
+ this._flagCallsPendingCapture.delete(key);
636
+ continue;
637
+ }
638
+ this.getFeatureFlagResult(key);
639
+ if (this._flagCallsPendingCapture.has(key)) break;
640
+ }
641
+ }
642
+ } finally{
643
+ this._sendingFlagCallsPendingCapture = false;
644
+ }
617
645
  }
618
646
  _callFlagsEndpoint(options) {
619
647
  this._clearDebouncer();
@@ -776,12 +804,17 @@ class PostHogFeatureFlags {
776
804
  [FLAG_CALL_REPORTED_SESSION_ID]: sessionIdToPersist
777
805
  });
778
806
  } else {
779
- const canCapture = this._client?.canCapture;
780
- if (!canCapture && Object.keys(flagCallReported).length > 0) {
781
- flagCallReported = {};
782
- this._set({
783
- [FLAG_CALL_REPORTED]: flagCallReported
784
- });
807
+ const captureOff = !this._client?.canCapture;
808
+ const canCapture = !captureOff && !this._client?.isRateLimited;
809
+ if (canCapture) this._flagCallsPendingCapture.delete(key);
810
+ else {
811
+ if (captureOff && Object.keys(flagCallReported).length > 0) {
812
+ flagCallReported = {};
813
+ this._set({
814
+ [FLAG_CALL_REPORTED]: flagCallReported
815
+ });
816
+ }
817
+ if (!flagCallReported[key]?.includes(flagReportValue)) this._holdFlagCall(key, flagReportValue);
785
818
  }
786
819
  if (canCapture && !flagCallReported[key]?.includes(flagReportValue)) {
787
820
  if (isArray(flagCallReported[key])) flagCallReported[key].push(flagReportValue);
@@ -817,7 +850,10 @@ class PostHogFeatureFlags {
817
850
  if (!isUndefined(flagDetails?.original_variant) || !isUndefined(flagDetails?.original_enabled)) properties.$feature_flag_original_response = isUndefined(flagDetails.original_variant) ? flagDetails.original_enabled : flagDetails.original_variant;
818
851
  if (flagDetails?.metadata?.original_payload) properties.$feature_flag_original_payload = flagDetails?.metadata?.original_payload;
819
852
  if (errors.length) properties.$feature_flag_error = errors.join(',');
820
- this._captureFeatureFlagCalled(properties);
853
+ if (!this._captureFeatureFlagCalled(properties)) {
854
+ this._unmarkFlagCallReported(key, flagReportValue);
855
+ this._holdFlagCall(key, flagReportValue);
856
+ }
821
857
  } else if (sessionIdToPersist) this._set({
822
858
  [FLAG_CALL_REPORTED]: flagCallReported,
823
859
  [FLAG_CALL_REPORTED_SESSION_ID]: sessionIdToPersist
@@ -833,11 +869,36 @@ class PostHogFeatureFlags {
833
869
  }
834
870
  _captureFeatureFlagCalled(properties) {
835
871
  try {
836
- this._client?.capture('$feature_flag_called', properties);
872
+ return this._client?.capture('$feature_flag_called', properties) !== false;
837
873
  } catch (error) {
838
874
  this._logger.error('Failed to capture feature flag call', error);
875
+ return true;
839
876
  }
840
877
  }
878
+ _holdFlagCall(key, value) {
879
+ this._flagCallsPendingCapture.set(key, {
880
+ value,
881
+ context: this._flagCallContext()
882
+ });
883
+ }
884
+ _flagCallContext() {
885
+ return {
886
+ distinctId: this._client?.distinctId,
887
+ groups: {
888
+ ...this._client?.groups
889
+ },
890
+ sessionId: this._config.deduplicateCallsPerSession ? this._client?.session?.sessionId : void 0
891
+ };
892
+ }
893
+ _unmarkFlagCallReported(key, flagReportValue) {
894
+ const flagCallReported = this._prop(FLAG_CALL_REPORTED) || {};
895
+ const reportedValues = flagCallReported[key];
896
+ if (!isArray(reportedValues) || !reportedValues.includes(flagReportValue)) return;
897
+ flagCallReported[key] = reportedValues.filter((value)=>value !== flagReportValue);
898
+ this._set({
899
+ [FLAG_CALL_REPORTED]: flagCallReported
900
+ });
901
+ }
841
902
  getRemoteConfigPayload(key, callback) {
842
903
  this._getRemoteConfigPayload(key, callback);
843
904
  }
@@ -1179,6 +1240,7 @@ class PostHogFeatureFlags {
1179
1240
  this._clearDebouncer();
1180
1241
  this._override_warning = false;
1181
1242
  this._consecutiveStatusZeroFailures = 0;
1243
+ this._flagCallsPendingCapture = new Map();
1182
1244
  }
1183
1245
  }
1184
1246
  export { FeatureFlagError, PostHogFeatureFlags, QuotaLimitedResource, filterActiveFeatureFlags, parseFlagsResponse };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@posthog/browser-common",
3
- "version": "0.10.4",
3
+ "version": "0.10.6",
4
4
  "description": "Internal shared browser utilities and extension primitives for PostHog Browser SDKs",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -241,8 +241,8 @@
241
241
  "dependencies": {
242
242
  "dompurify": "^3.4.13",
243
243
  "preact": "^10.29.3",
244
- "@posthog/types": "^1.419.0",
245
- "@posthog/core": "^1.62.1"
244
+ "@posthog/core": "^1.63.1",
245
+ "@posthog/types": "^1.419.0"
246
246
  },
247
247
  "devDependencies": {
248
248
  "@rsbuild/plugin-react": "^2.1.0",