@posthog/core 1.40.2 → 1.41.1

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.
Files changed (41) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/posthog-core-stateless.d.ts.map +1 -1
  4. package/dist/posthog-core.d.ts.map +1 -1
  5. package/dist/surveys/activation.d.ts +20 -0
  6. package/dist/surveys/activation.d.ts.map +1 -0
  7. package/dist/surveys/activation.js +45 -0
  8. package/dist/surveys/activation.mjs +8 -0
  9. package/dist/surveys/events.d.ts +1 -4
  10. package/dist/surveys/events.d.ts.map +1 -1
  11. package/dist/surveys/index.d.ts +2 -0
  12. package/dist/surveys/index.d.ts.map +1 -1
  13. package/dist/surveys/index.js +16 -2
  14. package/dist/surveys/index.mjs +3 -1
  15. package/dist/surveys/keys.d.ts +14 -0
  16. package/dist/surveys/keys.d.ts.map +1 -0
  17. package/dist/surveys/keys.js +45 -0
  18. package/dist/surveys/keys.mjs +8 -0
  19. package/dist/types.d.ts +87 -76
  20. package/dist/types.d.ts.map +1 -1
  21. package/dist/types.js +65 -76
  22. package/dist/types.mjs +66 -77
  23. package/dist/utils/bucketed-rate-limiter.d.ts +17 -0
  24. package/dist/utils/bucketed-rate-limiter.d.ts.map +1 -1
  25. package/dist/utils/bucketed-rate-limiter.js +18 -1
  26. package/dist/utils/bucketed-rate-limiter.mjs +9 -1
  27. package/dist/utils/type-utils.d.ts.map +1 -1
  28. package/package.json +2 -2
  29. package/src/index.ts +3 -0
  30. package/src/posthog-core-stateless.ts +0 -1
  31. package/src/posthog-core.ts +0 -1
  32. package/src/surveys/activation.spec.ts +61 -0
  33. package/src/surveys/activation.ts +28 -0
  34. package/src/surveys/events.ts +1 -5
  35. package/src/surveys/index.ts +2 -0
  36. package/src/surveys/keys.spec.ts +29 -0
  37. package/src/surveys/keys.ts +23 -0
  38. package/src/types.ts +88 -76
  39. package/src/utils/bucketed-rate-limiter.spec.ts +38 -1
  40. package/src/utils/bucketed-rate-limiter.ts +29 -0
  41. package/src/utils/type-utils.ts +0 -6
@@ -16,3 +16,5 @@ export {
16
16
  getLanguageFromStoredPersonProperties,
17
17
  normalizeLanguageCode,
18
18
  } from './translations'
19
+ export { canSurveyActivateRepeatedly, doesSurveyActivateByEvent } from './activation'
20
+ export { getSurveyIterationKey, isSurveyKeyForSurvey, type SurveyWithIteration } from './keys'
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from '@jest/globals'
2
+ import { getSurveyIterationKey, isSurveyKeyForSurvey } from './keys'
3
+
4
+ describe('getSurveyIterationKey', () => {
5
+ const cases: [number | null | undefined, string][] = [
6
+ [2, 'survey-1_2'],
7
+ [1, 'survey-1_1'],
8
+ [0, 'survey-1'],
9
+ [null, 'survey-1'],
10
+ [undefined, 'survey-1'],
11
+ ]
12
+
13
+ it.each(cases)('current_iteration %p produces key %p', (currentIteration, expected) => {
14
+ expect(getSurveyIterationKey({ id: 'survey-1', current_iteration: currentIteration })).toBe(expected)
15
+ })
16
+ })
17
+
18
+ describe('isSurveyKeyForSurvey', () => {
19
+ it.each([
20
+ ['bare id', 'abc', 'abc', true],
21
+ ['first iteration', 'abc_1', 'abc', true],
22
+ ['later iteration', 'abc_12', 'abc', true],
23
+ ['different survey id', 'def_1', 'abc', false],
24
+ ['prefix collision with bare id', 'abcd', 'abc', false],
25
+ ['prefix collision with iteration key', 'abcd_1', 'abc', false],
26
+ ])('%s: key %p for survey %p returns %p', (_name, key, surveyId, expected) => {
27
+ expect(isSurveyKeyForSurvey(key, surveyId)).toBe(expected)
28
+ })
29
+ })
@@ -0,0 +1,23 @@
1
+ import { Survey } from '../types'
2
+
3
+ export type SurveyWithIteration = Pick<Survey, 'id' | 'current_iteration'>
4
+
5
+ /**
6
+ * True when a stored display-state key belongs to the given survey: its bare id
7
+ * or any iteration-qualified `id_n` key.
8
+ */
9
+ export function isSurveyKeyForSurvey(key: string, surveyId: string): boolean {
10
+ return key === surveyId || key.startsWith(`${surveyId}_`)
11
+ }
12
+
13
+ /**
14
+ * Iteration-qualified survey identifier ('id' or 'id_iteration'), used to key
15
+ * per-survey display state (seen, in-progress, ...). Keying by iteration lets a
16
+ * repeating survey become visible again when a new iteration starts.
17
+ */
18
+ export function getSurveyIterationKey(survey: SurveyWithIteration): string {
19
+ if (survey.current_iteration && survey.current_iteration > 0) {
20
+ return `${survey.id}_${survey.current_iteration}`
21
+ }
22
+ return survey.id
23
+ }
package/src/types.ts CHANGED
@@ -633,43 +633,49 @@ export type SurveyAppearance = {
633
633
  widgetColor?: string
634
634
  }
635
635
 
636
- export enum SurveyPosition {
637
- TopLeft = 'top_left',
638
- TopCenter = 'top_center',
639
- TopRight = 'top_right',
640
- MiddleLeft = 'middle_left',
641
- MiddleCenter = 'middle_center',
642
- MiddleRight = 'middle_right',
643
- Left = 'left',
644
- Right = 'right',
645
- Center = 'center',
646
- }
636
+ export const SurveyPosition = {
637
+ TopLeft: 'top_left',
638
+ TopCenter: 'top_center',
639
+ TopRight: 'top_right',
640
+ MiddleLeft: 'middle_left',
641
+ MiddleCenter: 'middle_center',
642
+ MiddleRight: 'middle_right',
643
+ Left: 'left',
644
+ Right: 'right',
645
+ Center: 'center',
646
+ } as const
647
+ export type SurveyPosition = (typeof SurveyPosition)[keyof typeof SurveyPosition]
647
648
 
648
- export enum SurveyWidgetType {
649
- Button = 'button',
650
- Tab = 'tab',
651
- Selector = 'selector',
652
- }
649
+ export const SurveyWidgetType = {
650
+ Button: 'button',
651
+ Tab: 'tab',
652
+ Selector: 'selector',
653
+ } as const
654
+ export type SurveyWidgetType = (typeof SurveyWidgetType)[keyof typeof SurveyWidgetType]
653
655
 
654
- export enum SurveyType {
655
- Popover = 'popover',
656
- API = 'api',
657
- Widget = 'widget',
658
- ExternalSurvey = 'external_survey',
659
- }
656
+ export const SurveyType = {
657
+ Popover: 'popover',
658
+ API: 'api',
659
+ Widget: 'widget',
660
+ ExternalSurvey: 'external_survey',
661
+ } as const
662
+ export type SurveyType = (typeof SurveyType)[keyof typeof SurveyType]
660
663
 
661
664
  export type SurveyQuestion = BasicSurveyQuestion | LinkSurveyQuestion | RatingSurveyQuestion | MultipleSurveyQuestion
662
665
 
663
- export enum SurveyQuestionDescriptionContentType {
664
- Html = 'html',
665
- Text = 'text',
666
- }
666
+ export const SurveyQuestionDescriptionContentType = {
667
+ Html: 'html',
668
+ Text: 'text',
669
+ } as const
670
+ export type SurveyQuestionDescriptionContentType =
671
+ (typeof SurveyQuestionDescriptionContentType)[keyof typeof SurveyQuestionDescriptionContentType]
667
672
 
668
673
  // Survey validation types
669
- export enum SurveyValidationType {
670
- MinLength = 'min_length',
671
- MaxLength = 'max_length',
672
- }
674
+ export const SurveyValidationType = {
675
+ MinLength: 'min_length',
676
+ MaxLength: 'max_length',
677
+ } as const
678
+ export type SurveyValidationType = (typeof SurveyValidationType)[keyof typeof SurveyValidationType]
673
679
 
674
680
  export interface SurveyValidationRule {
675
681
  type: SurveyValidationType
@@ -710,16 +716,16 @@ type SurveyQuestionBase = {
710
716
  }
711
717
 
712
718
  export type BasicSurveyQuestion = SurveyQuestionBase & {
713
- type: SurveyQuestionType.Open
719
+ type: typeof SurveyQuestionType.Open
714
720
  }
715
721
 
716
722
  export type LinkSurveyQuestion = SurveyQuestionBase & {
717
- type: SurveyQuestionType.Link
723
+ type: typeof SurveyQuestionType.Link
718
724
  link?: string | null
719
725
  }
720
726
 
721
727
  export type RatingSurveyQuestion = SurveyQuestionBase & {
722
- type: SurveyQuestionType.Rating
728
+ type: typeof SurveyQuestionType.Rating
723
729
  display: SurveyRatingDisplay
724
730
  scale: 2 | 3 | 5 | 7 | 10
725
731
  lowerBoundLabel: string
@@ -727,49 +733,52 @@ export type RatingSurveyQuestion = SurveyQuestionBase & {
727
733
  skipSubmitButton?: boolean
728
734
  }
729
735
 
730
- export enum SurveyRatingDisplay {
731
- Number = 'number',
732
- Emoji = 'emoji',
733
- }
736
+ export const SurveyRatingDisplay = {
737
+ Number: 'number',
738
+ Emoji: 'emoji',
739
+ } as const
740
+ export type SurveyRatingDisplay = (typeof SurveyRatingDisplay)[keyof typeof SurveyRatingDisplay]
734
741
 
735
742
  export type MultipleSurveyQuestion = SurveyQuestionBase & {
736
- type: SurveyQuestionType.SingleChoice | SurveyQuestionType.MultipleChoice
743
+ type: typeof SurveyQuestionType.SingleChoice | typeof SurveyQuestionType.MultipleChoice
737
744
  choices: string[]
738
745
  hasOpenChoice?: boolean
739
746
  shuffleOptions?: boolean
740
747
  skipSubmitButton?: boolean
741
748
  }
742
749
 
743
- export enum SurveyQuestionType {
744
- Open = 'open',
745
- MultipleChoice = 'multiple_choice',
746
- SingleChoice = 'single_choice',
747
- Rating = 'rating',
748
- Link = 'link',
749
- }
750
+ export const SurveyQuestionType = {
751
+ Open: 'open',
752
+ MultipleChoice: 'multiple_choice',
753
+ SingleChoice: 'single_choice',
754
+ Rating: 'rating',
755
+ Link: 'link',
756
+ } as const
757
+ export type SurveyQuestionType = (typeof SurveyQuestionType)[keyof typeof SurveyQuestionType]
750
758
 
751
- export enum SurveyQuestionBranchingType {
752
- NextQuestion = 'next_question',
753
- End = 'end',
754
- ResponseBased = 'response_based',
755
- SpecificQuestion = 'specific_question',
756
- }
759
+ export const SurveyQuestionBranchingType = {
760
+ NextQuestion: 'next_question',
761
+ End: 'end',
762
+ ResponseBased: 'response_based',
763
+ SpecificQuestion: 'specific_question',
764
+ } as const
765
+ export type SurveyQuestionBranchingType = (typeof SurveyQuestionBranchingType)[keyof typeof SurveyQuestionBranchingType]
757
766
 
758
767
  export type NextQuestionBranching = {
759
- type: SurveyQuestionBranchingType.NextQuestion
768
+ type: typeof SurveyQuestionBranchingType.NextQuestion
760
769
  }
761
770
 
762
771
  export type EndBranching = {
763
- type: SurveyQuestionBranchingType.End
772
+ type: typeof SurveyQuestionBranchingType.End
764
773
  }
765
774
 
766
775
  export type ResponseBasedBranching = {
767
- type: SurveyQuestionBranchingType.ResponseBased
776
+ type: typeof SurveyQuestionBranchingType.ResponseBased
768
777
  responseValues: Record<string, any>
769
778
  }
770
779
 
771
780
  export type SpecificQuestionBranching = {
772
- type: SurveyQuestionBranchingType.SpecificQuestion
781
+ type: typeof SurveyQuestionBranchingType.SpecificQuestion
773
782
  index: number
774
783
  }
775
784
 
@@ -783,20 +792,22 @@ export type SurveyResponses = Record<string, SurveyResponseValue>
783
792
 
784
793
  export type SurveyCallback = (surveys: Survey[]) => void
785
794
 
786
- export enum SurveyMatchType {
787
- Regex = 'regex',
788
- NotRegex = 'not_regex',
789
- Exact = 'exact',
790
- IsNot = 'is_not',
791
- Icontains = 'icontains',
792
- NotIcontains = 'not_icontains',
793
- }
795
+ export const SurveyMatchType = {
796
+ Regex: 'regex',
797
+ NotRegex: 'not_regex',
798
+ Exact: 'exact',
799
+ IsNot: 'is_not',
800
+ Icontains: 'icontains',
801
+ NotIcontains: 'not_icontains',
802
+ } as const
803
+ export type SurveyMatchType = (typeof SurveyMatchType)[keyof typeof SurveyMatchType]
794
804
 
795
- export enum SurveySchedule {
796
- Once = 'once',
797
- Recurring = 'recurring',
798
- Always = 'always',
799
- }
805
+ export const SurveySchedule = {
806
+ Once: 'once',
807
+ Recurring: 'recurring',
808
+ Always: 'always',
809
+ } as const
810
+ export type SurveySchedule = (typeof SurveySchedule)[keyof typeof SurveySchedule]
800
811
 
801
812
  export type SurveyElement = {
802
813
  text?: string
@@ -853,9 +864,9 @@ export type Survey = {
853
864
  }
854
865
  start_date?: string
855
866
  end_date?: string
856
- current_iteration?: number
857
- current_iteration_start_date?: string
858
- schedule?: SurveySchedule
867
+ current_iteration?: number | null
868
+ current_iteration_start_date?: string | null
869
+ schedule?: SurveySchedule | null
859
870
  }
860
871
 
861
872
  export type SurveyActionType = {
@@ -865,11 +876,12 @@ export type SurveyActionType = {
865
876
  }
866
877
 
867
878
  /** Sync with plugin-server/src/types.ts */
868
- export enum ActionStepStringMatching {
869
- Contains = 'contains',
870
- Exact = 'exact',
871
- Regex = 'regex',
872
- }
879
+ export const ActionStepStringMatching = {
880
+ Contains: 'contains',
881
+ Exact: 'exact',
882
+ Regex: 'regex',
883
+ } as const
884
+ export type ActionStepStringMatching = (typeof ActionStepStringMatching)[keyof typeof ActionStepStringMatching]
873
885
 
874
886
  export type ActionStepType = {
875
887
  event?: string
@@ -1,8 +1,45 @@
1
1
  import { Logger } from '@/types'
2
- import { BucketedRateLimiter } from './bucketed-rate-limiter'
2
+ import {
3
+ BucketedRateLimiter,
4
+ DEFAULT_EXCEPTION_RATE_LIMITER_BUCKET_SIZE,
5
+ DEFAULT_EXCEPTION_RATE_LIMITER_REFILL_RATE,
6
+ resolveExceptionRateLimiterConfig,
7
+ } from './bucketed-rate-limiter'
3
8
 
4
9
  jest.useFakeTimers()
5
10
 
11
+ describe('resolveExceptionRateLimiterConfig', () => {
12
+ it('falls back to the shared defaults when nothing is configured', () => {
13
+ expect(resolveExceptionRateLimiterConfig()).toEqual({
14
+ refillRate: DEFAULT_EXCEPTION_RATE_LIMITER_REFILL_RATE,
15
+ bucketSize: DEFAULT_EXCEPTION_RATE_LIMITER_BUCKET_SIZE,
16
+ })
17
+ })
18
+
19
+ it('prefers the first-class options', () => {
20
+ expect(
21
+ resolveExceptionRateLimiterConfig({ exceptionRateLimiterRefillRate: 2, exceptionRateLimiterBucketSize: 20 })
22
+ ).toEqual({ refillRate: 2, bucketSize: 20 })
23
+ })
24
+
25
+ it('honours the deprecated double-underscore options as a fallback', () => {
26
+ expect(
27
+ resolveExceptionRateLimiterConfig({ __exceptionRateLimiterRefillRate: 3, __exceptionRateLimiterBucketSize: 30 })
28
+ ).toEqual({ refillRate: 3, bucketSize: 30 })
29
+ })
30
+
31
+ it('lets the first-class options win over the deprecated ones', () => {
32
+ expect(
33
+ resolveExceptionRateLimiterConfig({
34
+ exceptionRateLimiterRefillRate: 5,
35
+ __exceptionRateLimiterRefillRate: 3,
36
+ exceptionRateLimiterBucketSize: 50,
37
+ __exceptionRateLimiterBucketSize: 30,
38
+ })
39
+ ).toEqual({ refillRate: 5, bucketSize: 50 })
40
+ })
41
+ })
42
+
6
43
  describe('BucketedRateLimiter', () => {
7
44
  let rateLimiter: BucketedRateLimiter<string>
8
45
 
@@ -1,9 +1,38 @@
1
+ import type { ExceptionRateLimiterConfig } from '@posthog/types'
1
2
  import { Logger } from '../types'
2
3
  import { clampToRange } from './number-utils'
3
4
 
4
5
  type Bucket = { tokens: number; lastAccess: number }
5
6
  const ONE_DAY_IN_MS = 86400000
6
7
 
8
+ export const DEFAULT_EXCEPTION_RATE_LIMITER_REFILL_RATE = 1
9
+ export const DEFAULT_EXCEPTION_RATE_LIMITER_BUCKET_SIZE = 10
10
+
11
+ /**
12
+ * Resolves the error tracking rate limiter's `refillRate` and `bucketSize` from SDK config,
13
+ * applying the shared defaults. The deprecated double-underscore options are honoured as a
14
+ * fallback so existing browser SDK configs keep working after the rename.
15
+ */
16
+ export function resolveExceptionRateLimiterConfig(
17
+ config: ExceptionRateLimiterConfig & {
18
+ /** @deprecated use exceptionRateLimiterRefillRate */
19
+ __exceptionRateLimiterRefillRate?: number
20
+ /** @deprecated use exceptionRateLimiterBucketSize */
21
+ __exceptionRateLimiterBucketSize?: number
22
+ } = {}
23
+ ): { refillRate: number; bucketSize: number } {
24
+ return {
25
+ refillRate:
26
+ config.exceptionRateLimiterRefillRate ??
27
+ config.__exceptionRateLimiterRefillRate ??
28
+ DEFAULT_EXCEPTION_RATE_LIMITER_REFILL_RATE,
29
+ bucketSize:
30
+ config.exceptionRateLimiterBucketSize ??
31
+ config.__exceptionRateLimiterBucketSize ??
32
+ DEFAULT_EXCEPTION_RATE_LIMITER_BUCKET_SIZE,
33
+ }
34
+ }
35
+
7
36
  export class BucketedRateLimiter<T extends string | number> {
8
37
  private _bucketSize: number
9
38
  private _refillRate: number
@@ -6,7 +6,6 @@ import {
6
6
  } from '../types'
7
7
  import { includes } from './string-utils'
8
8
 
9
- // eslint-disable-next-line posthog-js/no-direct-array-check
10
9
  const nativeIsArray = Array.isArray
11
10
  const ObjProto = Object.prototype
12
11
  export const hasOwnProperty = ObjProto.hasOwnProperty
@@ -22,7 +21,6 @@ export const isArray =
22
21
  // fails on only one very rare and deliberate custom object:
23
22
  // let bomb = { toString : undefined, valueOf: function(o) { return "function BOMBA!"; }};
24
23
  export const isFunction = (x: unknown): x is (...args: any[]) => any => {
25
- // eslint-disable-next-line posthog-js/no-direct-function-check
26
24
  return typeof x === 'function'
27
25
  }
28
26
 
@@ -53,9 +51,7 @@ export const isString = (x: unknown): x is string => {
53
51
  }
54
52
 
55
53
  export const isEmptyString = (x: unknown): boolean => isString(x) && x.trim().length === 0
56
-
57
54
  export const isNull = (x: unknown): x is null => {
58
- // eslint-disable-next-line posthog-js/no-direct-null-check
59
55
  return x === null
60
56
  }
61
57
 
@@ -66,7 +62,6 @@ export const isNull = (x: unknown): x is null => {
66
62
  export const isNullish = (x: unknown): x is null | undefined => isUndefined(x) || isNull(x)
67
63
 
68
64
  export const isNumber = (x: unknown): x is number => {
69
- // eslint-disable-next-line posthog-js/no-direct-number-check
70
65
  // x !== x is true only for NaN (ES5-compatible NaN check)
71
66
  return toString.call(x) == '[object Number]' && x === x
72
67
  }
@@ -76,7 +71,6 @@ export const isPositiveNumber = (value: unknown): value is number => {
76
71
  }
77
72
 
78
73
  export const isBoolean = (x: unknown): x is boolean => {
79
- // eslint-disable-next-line posthog-js/no-direct-boolean-check
80
74
  return toString.call(x) === '[object Boolean]'
81
75
  }
82
76