@posthog/core 1.27.9 → 1.28.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 (55) hide show
  1. package/dist/index.d.ts +2 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/logs/index.d.ts +94 -4
  4. package/dist/logs/index.d.ts.map +1 -1
  5. package/dist/logs/index.js +149 -4
  6. package/dist/logs/index.mjs +150 -5
  7. package/dist/logs/logs-utils.d.ts +2 -1
  8. package/dist/logs/logs-utils.d.ts.map +1 -1
  9. package/dist/logs/types.d.ts +140 -8
  10. package/dist/logs/types.d.ts.map +1 -1
  11. package/dist/posthog-core-stateless.d.ts +34 -0
  12. package/dist/posthog-core-stateless.d.ts.map +1 -1
  13. package/dist/posthog-core-stateless.js +46 -0
  14. package/dist/posthog-core-stateless.mjs +46 -0
  15. package/dist/posthog-core.d.ts.map +1 -1
  16. package/dist/posthog-core.js +2 -0
  17. package/dist/posthog-core.mjs +2 -0
  18. package/dist/surveys/events.d.ts +22 -0
  19. package/dist/surveys/events.d.ts.map +1 -0
  20. package/dist/surveys/events.js +95 -0
  21. package/dist/surveys/events.mjs +43 -0
  22. package/dist/surveys/index.d.ts +4 -0
  23. package/dist/surveys/index.d.ts.map +1 -0
  24. package/dist/surveys/index.js +83 -0
  25. package/dist/surveys/index.mjs +4 -0
  26. package/dist/surveys/translations.d.ts +38 -0
  27. package/dist/surveys/translations.d.ts.map +1 -0
  28. package/dist/surveys/translations.js +207 -0
  29. package/dist/surveys/translations.mjs +158 -0
  30. package/dist/testing/test-utils.d.ts.map +1 -1
  31. package/dist/testing/test-utils.js +1 -0
  32. package/dist/testing/test-utils.mjs +1 -0
  33. package/dist/types.d.ts +31 -2
  34. package/dist/types.d.ts.map +1 -1
  35. package/dist/utils/logger.d.ts +1 -1
  36. package/dist/utils/logger.d.ts.map +1 -1
  37. package/dist/utils/logger.js +3 -0
  38. package/dist/utils/logger.mjs +3 -0
  39. package/package.json +26 -2
  40. package/src/index.ts +12 -1
  41. package/src/logs/index.spec.ts +891 -17
  42. package/src/logs/index.ts +341 -11
  43. package/src/logs/logs-utils.spec.ts +2 -1
  44. package/src/logs/logs-utils.ts +1 -1
  45. package/src/logs/types.ts +150 -25
  46. package/src/posthog-core-stateless.ts +80 -0
  47. package/src/posthog-core.ts +6 -0
  48. package/src/surveys/events.spec.ts +52 -0
  49. package/src/surveys/events.ts +80 -0
  50. package/src/surveys/index.ts +18 -0
  51. package/src/surveys/translations.spec.ts +205 -0
  52. package/src/surveys/translations.ts +244 -0
  53. package/src/testing/test-utils.ts +1 -0
  54. package/src/types.ts +38 -2
  55. package/src/utils/logger.ts +6 -2
@@ -0,0 +1,244 @@
1
+ import { SurveyQuestionTranslation, SurveyTranslation } from '../types'
2
+ import { createLogger, isArray, isUndefined } from '../utils'
3
+
4
+ const logger = createLogger('[SurveyTranslations]')
5
+
6
+ export type DetectSurveyLanguageOptions = {
7
+ overrideLanguage?: unknown
8
+ storedPersonProperties?: unknown
9
+ locale?: unknown
10
+ }
11
+
12
+ function getTrimmedLanguage(value: unknown): string | null {
13
+ return typeof value === 'string' && value.trim() ? value.trim() : null
14
+ }
15
+
16
+ export function getLanguageFromStoredPersonProperties(storedPersonProperties: unknown): string | null {
17
+ if (
18
+ !storedPersonProperties ||
19
+ typeof storedPersonProperties !== 'object' ||
20
+ !('language' in storedPersonProperties)
21
+ ) {
22
+ return null
23
+ }
24
+
25
+ return getTrimmedLanguage(storedPersonProperties.language)
26
+ }
27
+
28
+ export function detectSurveyLanguage({
29
+ overrideLanguage,
30
+ storedPersonProperties,
31
+ locale,
32
+ }: DetectSurveyLanguageOptions): string | null {
33
+ const explicitLanguage = getTrimmedLanguage(overrideLanguage)
34
+ if (explicitLanguage) {
35
+ logger.info(`Using override display language: ${explicitLanguage}`)
36
+ return explicitLanguage
37
+ }
38
+
39
+ const personLanguage = getLanguageFromStoredPersonProperties(storedPersonProperties)
40
+ if (personLanguage) {
41
+ logger.info(`Using person property language: ${personLanguage}`)
42
+ return personLanguage
43
+ }
44
+
45
+ const detectedLocale = getTrimmedLanguage(locale)
46
+ if (detectedLocale) {
47
+ logger.info(`Using detected locale: ${detectedLocale}`)
48
+ return detectedLocale
49
+ }
50
+
51
+ logger.info('No user language detected')
52
+ return null
53
+ }
54
+
55
+ export function normalizeLanguageCode(languageCode: string): string {
56
+ return languageCode.toLowerCase()
57
+ }
58
+
59
+ export function getBaseLanguage(languageCode: string): string {
60
+ return languageCode.split('-')[0]
61
+ }
62
+
63
+ export function findBestTranslationMatch(
64
+ translations: Record<string, unknown> | undefined,
65
+ targetLanguage: string
66
+ ): string | null {
67
+ if (!translations || !targetLanguage) {
68
+ return null
69
+ }
70
+
71
+ const normalizedTarget = normalizeLanguageCode(targetLanguage)
72
+
73
+ const exactMatch = Object.keys(translations).find((key) => normalizeLanguageCode(key) === normalizedTarget)
74
+ if (exactMatch) {
75
+ logger.debug(`Found exact translation match: ${exactMatch}`)
76
+ return exactMatch
77
+ }
78
+
79
+ if (normalizedTarget.includes('-')) {
80
+ const baseLanguage = getBaseLanguage(normalizedTarget)
81
+ const baseMatch = Object.keys(translations).find((key) => normalizeLanguageCode(key) === baseLanguage)
82
+ if (baseMatch) {
83
+ logger.debug(`Found base language translation match: ${baseMatch} (from ${targetLanguage})`)
84
+ return baseMatch
85
+ }
86
+ }
87
+
88
+ return null
89
+ }
90
+
91
+ function isTranslatedChoices(
92
+ questionTranslation: SurveyQuestionTranslation
93
+ ): questionTranslation is SurveyQuestionTranslation & { choices: string[] } {
94
+ return isArray(questionTranslation.choices)
95
+ }
96
+
97
+ function hasThankYouTranslation(translation: SurveyTranslation): boolean {
98
+ return (
99
+ !isUndefined(translation.thankYouMessageHeader) ||
100
+ !isUndefined(translation.thankYouMessageDescription) ||
101
+ !isUndefined(translation.thankYouMessageCloseButtonText)
102
+ )
103
+ }
104
+
105
+ type TranslatableSurveyAppearance = {
106
+ thankYouMessageHeader?: string
107
+ thankYouMessageDescription?: string | null
108
+ thankYouMessageCloseButtonText?: string
109
+ }
110
+
111
+ type TranslatableSurveyQuestion = {
112
+ question: string
113
+ description?: string | null
114
+ buttonText?: string
115
+ link?: string | null
116
+ lowerBoundLabel?: string
117
+ upperBoundLabel?: string
118
+ choices?: string[]
119
+ translations?: Record<string, SurveyQuestionTranslation>
120
+ }
121
+
122
+ type TranslatableSurvey<TQuestion extends TranslatableSurveyQuestion = TranslatableSurveyQuestion> = {
123
+ name: string
124
+ translations?: Record<string, SurveyTranslation>
125
+ appearance?: TranslatableSurveyAppearance | null
126
+ questions: TQuestion[]
127
+ }
128
+
129
+ function mergeQuestionTranslation<TQuestion extends TranslatableSurveyQuestion>(
130
+ question: TQuestion,
131
+ targetLanguage: string
132
+ ): { question: TQuestion; matchedKey: string | null; hasChanges: boolean } {
133
+ const translationKey = findBestTranslationMatch(question.translations, targetLanguage)
134
+ if (!translationKey) {
135
+ return { question, matchedKey: null, hasChanges: false }
136
+ }
137
+
138
+ const questionTranslation = question.translations?.[translationKey]
139
+ if (!questionTranslation) {
140
+ return { question, matchedKey: null, hasChanges: false }
141
+ }
142
+
143
+ const translated: TQuestion = { ...question }
144
+ let hasChanges = false
145
+
146
+ if (!isUndefined(questionTranslation.question)) {
147
+ translated.question = questionTranslation.question
148
+ hasChanges = true
149
+ }
150
+ if (!isUndefined(questionTranslation.description)) {
151
+ translated.description = questionTranslation.description
152
+ hasChanges = true
153
+ }
154
+ if (!isUndefined(questionTranslation.buttonText)) {
155
+ translated.buttonText = questionTranslation.buttonText
156
+ hasChanges = true
157
+ }
158
+
159
+ if ('link' in translated && !isUndefined(questionTranslation.link)) {
160
+ translated.link = questionTranslation.link
161
+ hasChanges = true
162
+ }
163
+
164
+ if ('lowerBoundLabel' in translated && !isUndefined(questionTranslation.lowerBoundLabel)) {
165
+ translated.lowerBoundLabel = questionTranslation.lowerBoundLabel
166
+ hasChanges = true
167
+ }
168
+ if ('upperBoundLabel' in translated && !isUndefined(questionTranslation.upperBoundLabel)) {
169
+ translated.upperBoundLabel = questionTranslation.upperBoundLabel
170
+ hasChanges = true
171
+ }
172
+
173
+ if ('choices' in translated && isTranslatedChoices(questionTranslation)) {
174
+ translated.choices = questionTranslation.choices
175
+ hasChanges = true
176
+ }
177
+
178
+ return {
179
+ question: hasChanges ? translated : question,
180
+ matchedKey: hasChanges ? translationKey : null,
181
+ hasChanges,
182
+ }
183
+ }
184
+
185
+ export function applySurveyTranslation<
186
+ TQuestion extends TranslatableSurveyQuestion,
187
+ TSurvey extends TranslatableSurvey<TQuestion>,
188
+ >(survey: TSurvey, targetLanguage: string): { survey: TSurvey; matchedKey: string | null } {
189
+ const translationKey = findBestTranslationMatch(survey.translations, targetLanguage)
190
+
191
+ const translated: TSurvey = { ...survey }
192
+ let hasTranslation = false
193
+
194
+ if (translationKey) {
195
+ const translation = survey.translations?.[translationKey]
196
+ if (translation) {
197
+ logger.info(`Applying survey-level translation for language: ${translationKey}`)
198
+
199
+ if (!isUndefined(translation.name)) {
200
+ translated.name = translation.name
201
+ hasTranslation = true
202
+ }
203
+
204
+ if (translated.appearance) {
205
+ translated.appearance = { ...translated.appearance }
206
+
207
+ if (!isUndefined(translation.thankYouMessageHeader)) {
208
+ translated.appearance.thankYouMessageHeader = translation.thankYouMessageHeader
209
+ hasTranslation = true
210
+ }
211
+ if (!isUndefined(translation.thankYouMessageDescription)) {
212
+ translated.appearance.thankYouMessageDescription = translation.thankYouMessageDescription
213
+ hasTranslation = true
214
+ }
215
+ if (!isUndefined(translation.thankYouMessageCloseButtonText)) {
216
+ translated.appearance.thankYouMessageCloseButtonText = translation.thankYouMessageCloseButtonText
217
+ hasTranslation = true
218
+ }
219
+ } else if (hasThankYouTranslation(translation)) {
220
+ hasTranslation = true
221
+ }
222
+ }
223
+ }
224
+
225
+ const translatedResults = survey.questions.map((question) => mergeQuestionTranslation(question, targetLanguage))
226
+ const translatedQuestions = translatedResults.map((result) => result.question)
227
+ const anyQuestionTranslated = translatedResults.some((result) => result.hasChanges)
228
+
229
+ let questionMatchedKey: string | null = null
230
+ if (!translationKey) {
231
+ questionMatchedKey = translatedResults.find((result) => result.matchedKey)?.matchedKey || null
232
+ }
233
+
234
+ if (anyQuestionTranslated) {
235
+ translated.questions = translatedQuestions
236
+ hasTranslation = true
237
+ logger.info(`Applied question-level translations for language: ${targetLanguage}`)
238
+ }
239
+
240
+ return {
241
+ survey: translated,
242
+ matchedKey: hasTranslation ? translationKey || questionMatchedKey : null,
243
+ }
244
+ }
@@ -36,6 +36,7 @@ export const delay = (ms: number): Promise<void> => {
36
36
 
37
37
  export const createMockLogger = (): Logger => {
38
38
  return {
39
+ debug: jest.fn((...args) => console.debug(...args)),
39
40
  info: jest.fn((...args) => console.log(...args)),
40
41
  warn: jest.fn((...args) => console.warn(...args)),
41
42
  error: jest.fn((...args) => console.error(...args)),
package/src/types.ts CHANGED
@@ -344,6 +344,18 @@ export type PostHogRemoteConfig = {
344
344
  | {
345
345
  [key: string]: JsonType
346
346
  }
347
+
348
+ /**
349
+ * Logs feature remote config. When a map, `captureConsoleLogs` (boolean)
350
+ * is the local opt-in flag for `console.*` autocapture (read by the JS
351
+ * SDK's `PostHogLogs` extension to decide whether to load the autocapture
352
+ * bundle).
353
+ */
354
+ logs?:
355
+ | boolean
356
+ | {
357
+ [key: string]: JsonType
358
+ }
347
359
  }
348
360
 
349
361
  export type FeatureFlagValue = string | boolean
@@ -606,16 +618,34 @@ export interface SurveyValidationRule {
606
618
  errorMessage?: string
607
619
  }
608
620
 
621
+ export interface SurveyTranslation {
622
+ name?: string
623
+ thankYouMessageHeader?: string
624
+ thankYouMessageDescription?: string
625
+ thankYouMessageCloseButtonText?: string
626
+ }
627
+
628
+ export interface SurveyQuestionTranslation {
629
+ question?: string
630
+ description?: string | null
631
+ buttonText?: string
632
+ link?: string | null
633
+ lowerBoundLabel?: string
634
+ upperBoundLabel?: string
635
+ choices?: string[]
636
+ }
637
+
609
638
  type SurveyQuestionBase = {
610
639
  question: string
611
640
  id: string
612
- description?: string
641
+ description?: string | null
613
642
  descriptionContentType?: SurveyQuestionDescriptionContentType
614
643
  optional?: boolean
615
644
  buttonText?: string
616
645
  originalQuestionIndex: number
617
646
  branching?: NextQuestionBranching | EndBranching | ResponseBasedBranching | SpecificQuestionBranching
618
647
  validation?: SurveyValidationRule[]
648
+ translations?: Record<string, SurveyQuestionTranslation>
619
649
  }
620
650
 
621
651
  export type BasicSurveyQuestion = SurveyQuestionBase & {
@@ -624,7 +654,7 @@ export type BasicSurveyQuestion = SurveyQuestionBase & {
624
654
 
625
655
  export type LinkSurveyQuestion = SurveyQuestionBase & {
626
656
  type: SurveyQuestionType.Link
627
- link?: string
657
+ link?: string | null
628
658
  }
629
659
 
630
660
  export type RatingSurveyQuestion = SurveyQuestionBase & {
@@ -686,6 +716,10 @@ export type SurveyResponse = {
686
716
  surveys: Survey[]
687
717
  }
688
718
 
719
+ export type SurveyResponseValue = string | number | string[] | null
720
+
721
+ export type SurveyResponses = Record<string, SurveyResponseValue>
722
+
689
723
  export type SurveyCallback = (surveys: Survey[]) => void
690
724
 
691
725
  export enum SurveyMatchType {
@@ -728,6 +762,7 @@ export type Survey = {
728
762
  name: string
729
763
  description?: string
730
764
  type: SurveyType
765
+ translations?: Record<string, SurveyTranslation>
731
766
  feature_flag_keys?: {
732
767
  key: string
733
768
  value?: string
@@ -790,6 +825,7 @@ export type ActionStepType = {
790
825
  }
791
826
 
792
827
  export type Logger = {
828
+ debug: (...args: any[]) => void
793
829
  info: (...args: any[]) => void
794
830
  warn: (...args: any[]) => void
795
831
  error: (...args: any[]) => void
@@ -2,10 +2,10 @@ import { Logger } from '../types'
2
2
 
3
3
  // We want to make sure to get the original console methods as soon as possible
4
4
  type ConsoleLike = {
5
+ debug: (...args: any[]) => void
5
6
  log: (...args: any[]) => void
6
7
  warn: (...args: any[]) => void
7
8
  error: (...args: any[]) => void
8
- debug: (...args: any[]) => void
9
9
  }
10
10
 
11
11
  function createConsole(consoleLike: ConsoleLike = console): ConsoleLike {
@@ -23,7 +23,7 @@ export const _createLogger = (
23
23
  maybeCall: (fn: () => void) => void,
24
24
  consoleLike: ConsoleLike
25
25
  ): Logger => {
26
- function _log(level: 'log' | 'warn' | 'error', ...args: any[]) {
26
+ function _log(level: 'debug' | 'log' | 'warn' | 'error', ...args: any[]) {
27
27
  maybeCall(() => {
28
28
  const consoleMethod = consoleLike[level]
29
29
  consoleMethod(prefix, ...args)
@@ -31,6 +31,10 @@ export const _createLogger = (
31
31
  }
32
32
 
33
33
  const logger: Logger = {
34
+ debug: (...args: any[]) => {
35
+ _log('debug', ...args)
36
+ },
37
+
34
38
  info: (...args: any[]) => {
35
39
  _log('log', ...args)
36
40
  },