@posthog/core 1.18.0 → 1.20.0

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.
@@ -10,6 +10,7 @@ export declare abstract class PostHogCore extends PostHogCoreStateless {
10
10
  private _sessionMaxLengthSeconds;
11
11
  protected sessionProps: PostHogEventProperties;
12
12
  protected _personProfiles: 'always' | 'identified_only' | 'never';
13
+ protected _cachedPersonProperties: string | null;
13
14
  constructor(apiKey: string, options?: PostHogCoreOptions);
14
15
  protected setupBootstrap(options?: Partial<PostHogCoreOptions>): void;
15
16
  private clearProps;
@@ -73,8 +74,8 @@ export declare abstract class PostHogCore extends PostHogCoreStateless {
73
74
  * PROPERTIES
74
75
  ***/
75
76
  setPersonPropertiesForFlags(properties: {
76
- [type: string]: string;
77
- }): void;
77
+ [type: string]: JsonType;
78
+ }, reloadFeatureFlags?: boolean): void;
78
79
  resetPersonPropertiesForFlags(): void;
79
80
  setGroupPropertiesForFlags(properties: {
80
81
  [type: string]: Record<string, string>;
@@ -223,6 +224,47 @@ export declare abstract class PostHogCore extends PostHogCoreStateless {
223
224
  * @public
224
225
  */
225
226
  createPersonProfile(): void;
227
+ /**
228
+ * Sets properties on the person profile associated with the current `distinct_id`.
229
+ * Learn more about [identifying users](https://posthog.com/docs/product-analytics/identify)
230
+ *
231
+ * {@label Identification}
232
+ *
233
+ * @remarks
234
+ * Updates user properties that are stored with the person profile in PostHog.
235
+ * If `personProfiles` is set to `identified_only` and no profile exists, this will create one.
236
+ *
237
+ * @example
238
+ * ```js
239
+ * // set user properties
240
+ * posthog.setPersonProperties({
241
+ * email: 'user@example.com',
242
+ * plan: 'premium'
243
+ * })
244
+ * ```
245
+ *
246
+ * @example
247
+ * ```js
248
+ * // set properties with $set_once
249
+ * posthog.setPersonProperties(
250
+ * { name: 'Max Hedgehog' }, // $set properties
251
+ * { initial_url: '/blog' } // $set_once properties
252
+ * )
253
+ * ```
254
+ *
255
+ * @public
256
+ *
257
+ * @param userPropertiesToSet - Optional: An object of properties to store about the user.
258
+ * These properties will overwrite any existing values for the same keys.
259
+ * @param userPropertiesToSetOnce - Optional: An object of properties to store about the user.
260
+ * If a property is previously set, this does not override that value.
261
+ * @param reloadFeatureFlags - Whether to reload feature flags after setting the properties. Defaults to true.
262
+ */
263
+ setPersonProperties(userPropertiesToSet?: {
264
+ [key: string]: JsonType;
265
+ }, userPropertiesToSetOnce?: {
266
+ [key: string]: JsonType;
267
+ }, reloadFeatureFlags?: boolean): void;
226
268
  /**
227
269
  * Override processBeforeEnqueue to run before_send hooks.
228
270
  * This runs after prepareMessage, giving users full control over the final event.
@@ -1 +1 @@
1
- {"version":3,"file":"posthog-core.d.ts","sourceRoot":"","sources":["../src/posthog-core.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"posthog-core.d.ts","sourceRoot":"","sources":["../src/posthog-core.ts"],"names":[],"mappings":"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"}
@@ -39,7 +39,7 @@ class PostHogCore extends external_posthog_core_stateless_js_namespaceObject.Pos
39
39
  ...options,
40
40
  disableGeoip: disableGeoipOption,
41
41
  featureFlagsRequestTimeoutMs
42
- }), this.flagCallReported = {}, this._sessionMaxLengthSeconds = 86400, this.sessionProps = {};
42
+ }), this.flagCallReported = {}, this._sessionMaxLengthSeconds = 86400, this.sessionProps = {}, this._cachedPersonProperties = null;
43
43
  this.sendFeatureFlagEvent = options?.sendFeatureFlagEvent ?? true;
44
44
  this._sessionExpirationTimeSeconds = options?.sessionExpirationTimeSeconds ?? 1800;
45
45
  this._personProfiles = options?.personProfiles ?? 'identified_only';
@@ -94,6 +94,7 @@ class PostHogCore extends external_posthog_core_stateless_js_namespaceObject.Pos
94
94
  ...propertiesToKeep || []
95
95
  ];
96
96
  this.clearProps();
97
+ this._cachedPersonProperties = null;
97
98
  for (const key of Object.keys(external_types_js_namespaceObject.PostHogPersistedProperty))if (!allPropertiesToKeep.includes(external_types_js_namespaceObject.PostHogPersistedProperty[key])) this.setPersistedProperty(external_types_js_namespaceObject.PostHogPersistedProperty[key], null);
98
99
  this.reloadFeatureFlags();
99
100
  });
@@ -176,13 +177,16 @@ class PostHogCore extends external_posthog_core_stateless_js_namespaceObject.Pos
176
177
  ...(0, external_posthog_core_stateless_js_namespaceObject.maybeAdd)('$set', userProps),
177
178
  ...(0, external_posthog_core_stateless_js_namespaceObject.maybeAdd)('$set_once', userPropsOnce)
178
179
  });
180
+ const userPropsObj = (0, index_js_namespaceObject.isObject)(userProps) ? userProps : void 0;
181
+ const userPropsOnceObj = (0, index_js_namespaceObject.isObject)(userPropsOnce) ? userPropsOnce : void 0;
179
182
  if (distinctId !== previousDistinctId) {
180
183
  this.setPersistedProperty(external_types_js_namespaceObject.PostHogPersistedProperty.AnonymousId, previousDistinctId);
181
184
  this.setPersistedProperty(external_types_js_namespaceObject.PostHogPersistedProperty.DistinctId, distinctId);
182
185
  this.setPersistedProperty(external_types_js_namespaceObject.PostHogPersistedProperty.PersonMode, 'identified');
183
186
  this.reloadFeatureFlags();
184
- }
185
- super.identifyStateless(distinctId, allProperties, options);
187
+ super.identifyStateless(distinctId, allProperties, options);
188
+ this._cachedPersonProperties = (0, index_js_namespaceObject.getPersonPropertiesHash)(distinctId, userPropsObj, userPropsOnceObj);
189
+ } else if (userPropsObj || userPropsOnceObj) this.setPersonProperties(userPropsObj, userPropsOnceObj);
186
190
  });
187
191
  }
188
192
  capture(event, properties, options) {
@@ -249,13 +253,14 @@ class PostHogCore extends external_posthog_core_stateless_js_namespaceObject.Pos
249
253
  super.groupIdentifyStateless(groupType, groupKey, groupProperties, options, distinctId, eventProperties);
250
254
  });
251
255
  }
252
- setPersonPropertiesForFlags(properties) {
256
+ setPersonPropertiesForFlags(properties, reloadFeatureFlags = true) {
253
257
  this.wrap(()=>{
254
258
  const existingProperties = this.getPersistedProperty(external_types_js_namespaceObject.PostHogPersistedProperty.PersonProperties) || {};
255
259
  this.setPersistedProperty(external_types_js_namespaceObject.PostHogPersistedProperty.PersonProperties, {
256
260
  ...existingProperties,
257
261
  ...properties
258
262
  });
263
+ if (reloadFeatureFlags) this.reloadFeatureFlags();
259
264
  });
260
265
  }
261
266
  resetPersonPropertiesForFlags() {
@@ -628,6 +633,26 @@ class PostHogCore extends external_posthog_core_stateless_js_namespaceObject.Pos
628
633
  $set_once: {}
629
634
  });
630
635
  }
636
+ setPersonProperties(userPropertiesToSet, userPropertiesToSetOnce, reloadFeatureFlags = true) {
637
+ this.wrap(()=>{
638
+ const isSetEmpty = (0, index_js_namespaceObject.isNullish)(userPropertiesToSet) || (0, index_js_namespaceObject.isEmptyObject)(userPropertiesToSet);
639
+ const isSetOnceEmpty = (0, index_js_namespaceObject.isNullish)(userPropertiesToSetOnce) || (0, index_js_namespaceObject.isEmptyObject)(userPropertiesToSetOnce);
640
+ if (isSetEmpty && isSetOnceEmpty) return;
641
+ if (!this._requirePersonProcessing('posthog.setPersonProperties')) return;
642
+ const hash = (0, index_js_namespaceObject.getPersonPropertiesHash)(this.getDistinctId(), userPropertiesToSet, userPropertiesToSetOnce);
643
+ if (this._cachedPersonProperties === hash) return void this._logger.info('A duplicate setPersonProperties call was made with the same properties. It has been ignored.');
644
+ const mergedProperties = {
645
+ ...userPropertiesToSetOnce || {},
646
+ ...userPropertiesToSet || {}
647
+ };
648
+ this.setPersonPropertiesForFlags(mergedProperties, reloadFeatureFlags);
649
+ this.capture('$set', {
650
+ $set: userPropertiesToSet || {},
651
+ $set_once: userPropertiesToSetOnce || {}
652
+ });
653
+ this._cachedPersonProperties = hash;
654
+ });
655
+ }
631
656
  processBeforeEnqueue(message) {
632
657
  if (!this._beforeSend) return message;
633
658
  const timestamp = message.timestamp;
@@ -2,7 +2,7 @@ import { createFlagsResponseFromFlagsAndPayloads, getFeatureFlagValue, getFlagVa
2
2
  import { Compression, FeatureFlagError, PostHogPersistedProperty } from "./types.mjs";
3
3
  import { PostHogCoreStateless, QuotaLimitedFeature, maybeAdd } from "./posthog-core-stateless.mjs";
4
4
  import { uuidv7 } from "./vendor/uuidv7.mjs";
5
- import { isPlainError } from "./utils/index.mjs";
5
+ import { getPersonPropertiesHash, isEmptyObject, isNullish, isObject, isPlainError } from "./utils/index.mjs";
6
6
  class PostHogCore extends PostHogCoreStateless {
7
7
  constructor(apiKey, options){
8
8
  const disableGeoipOption = options?.disableGeoip ?? false;
@@ -11,7 +11,7 @@ class PostHogCore extends PostHogCoreStateless {
11
11
  ...options,
12
12
  disableGeoip: disableGeoipOption,
13
13
  featureFlagsRequestTimeoutMs
14
- }), this.flagCallReported = {}, this._sessionMaxLengthSeconds = 86400, this.sessionProps = {};
14
+ }), this.flagCallReported = {}, this._sessionMaxLengthSeconds = 86400, this.sessionProps = {}, this._cachedPersonProperties = null;
15
15
  this.sendFeatureFlagEvent = options?.sendFeatureFlagEvent ?? true;
16
16
  this._sessionExpirationTimeSeconds = options?.sessionExpirationTimeSeconds ?? 1800;
17
17
  this._personProfiles = options?.personProfiles ?? 'identified_only';
@@ -66,6 +66,7 @@ class PostHogCore extends PostHogCoreStateless {
66
66
  ...propertiesToKeep || []
67
67
  ];
68
68
  this.clearProps();
69
+ this._cachedPersonProperties = null;
69
70
  for (const key of Object.keys(PostHogPersistedProperty))if (!allPropertiesToKeep.includes(PostHogPersistedProperty[key])) this.setPersistedProperty(PostHogPersistedProperty[key], null);
70
71
  this.reloadFeatureFlags();
71
72
  });
@@ -148,13 +149,16 @@ class PostHogCore extends PostHogCoreStateless {
148
149
  ...maybeAdd('$set', userProps),
149
150
  ...maybeAdd('$set_once', userPropsOnce)
150
151
  });
152
+ const userPropsObj = isObject(userProps) ? userProps : void 0;
153
+ const userPropsOnceObj = isObject(userPropsOnce) ? userPropsOnce : void 0;
151
154
  if (distinctId !== previousDistinctId) {
152
155
  this.setPersistedProperty(PostHogPersistedProperty.AnonymousId, previousDistinctId);
153
156
  this.setPersistedProperty(PostHogPersistedProperty.DistinctId, distinctId);
154
157
  this.setPersistedProperty(PostHogPersistedProperty.PersonMode, 'identified');
155
158
  this.reloadFeatureFlags();
156
- }
157
- super.identifyStateless(distinctId, allProperties, options);
159
+ super.identifyStateless(distinctId, allProperties, options);
160
+ this._cachedPersonProperties = getPersonPropertiesHash(distinctId, userPropsObj, userPropsOnceObj);
161
+ } else if (userPropsObj || userPropsOnceObj) this.setPersonProperties(userPropsObj, userPropsOnceObj);
158
162
  });
159
163
  }
160
164
  capture(event, properties, options) {
@@ -221,13 +225,14 @@ class PostHogCore extends PostHogCoreStateless {
221
225
  super.groupIdentifyStateless(groupType, groupKey, groupProperties, options, distinctId, eventProperties);
222
226
  });
223
227
  }
224
- setPersonPropertiesForFlags(properties) {
228
+ setPersonPropertiesForFlags(properties, reloadFeatureFlags = true) {
225
229
  this.wrap(()=>{
226
230
  const existingProperties = this.getPersistedProperty(PostHogPersistedProperty.PersonProperties) || {};
227
231
  this.setPersistedProperty(PostHogPersistedProperty.PersonProperties, {
228
232
  ...existingProperties,
229
233
  ...properties
230
234
  });
235
+ if (reloadFeatureFlags) this.reloadFeatureFlags();
231
236
  });
232
237
  }
233
238
  resetPersonPropertiesForFlags() {
@@ -600,6 +605,26 @@ class PostHogCore extends PostHogCoreStateless {
600
605
  $set_once: {}
601
606
  });
602
607
  }
608
+ setPersonProperties(userPropertiesToSet, userPropertiesToSetOnce, reloadFeatureFlags = true) {
609
+ this.wrap(()=>{
610
+ const isSetEmpty = isNullish(userPropertiesToSet) || isEmptyObject(userPropertiesToSet);
611
+ const isSetOnceEmpty = isNullish(userPropertiesToSetOnce) || isEmptyObject(userPropertiesToSetOnce);
612
+ if (isSetEmpty && isSetOnceEmpty) return;
613
+ if (!this._requirePersonProcessing('posthog.setPersonProperties')) return;
614
+ const hash = getPersonPropertiesHash(this.getDistinctId(), userPropertiesToSet, userPropertiesToSetOnce);
615
+ if (this._cachedPersonProperties === hash) return void this._logger.info('A duplicate setPersonProperties call was made with the same properties. It has been ignored.');
616
+ const mergedProperties = {
617
+ ...userPropertiesToSetOnce || {},
618
+ ...userPropertiesToSet || {}
619
+ };
620
+ this.setPersonPropertiesForFlags(mergedProperties, reloadFeatureFlags);
621
+ this.capture('$set', {
622
+ $set: userPropertiesToSet || {},
623
+ $set_once: userPropertiesToSetOnce || {}
624
+ });
625
+ this._cachedPersonProperties = hash;
626
+ });
627
+ }
603
628
  processBeforeEnqueue(message) {
604
629
  if (!this._beforeSend) return message;
605
630
  const timestamp = message.timestamp;
@@ -1,6 +1,17 @@
1
+ import type { JsonType } from '../types';
1
2
  export declare function includes(str: string, needle: string): boolean;
2
3
  export declare function includes<T>(arr: T[], needle: T): boolean;
3
4
  export declare const trim: (str: string) => string;
4
5
  export declare const stripLeadingDollar: (s: string) => string;
5
6
  export declare function isDistinctIdStringLike(value: string): boolean;
7
+ /**
8
+ * Creates a hash string from distinct_id and person properties.
9
+ * Used to detect if person properties have changed to avoid duplicate $set events.
10
+ * Uses sorted keys to ensure consistent ordering regardless of object construction order.
11
+ */
12
+ export declare function getPersonPropertiesHash(distinct_id: string, userPropertiesToSet?: {
13
+ [key: string]: JsonType;
14
+ }, userPropertiesToSetOnce?: {
15
+ [key: string]: JsonType;
16
+ }): string;
6
17
  //# sourceMappingURL=string-utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"string-utils.d.ts","sourceRoot":"","sources":["../../src/utils/string-utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAA;AAC9D,wBAAgB,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,GAAG,OAAO,CAAA;AAKzD,eAAO,MAAM,IAAI,GAAa,KAAK,MAAM,KAAG,MAM3C,CAAA;AAID,eAAO,MAAM,kBAAkB,GAAa,GAAG,MAAM,KAAG,MAEvD,CAAA;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE7D"}
1
+ {"version":3,"file":"string-utils.d.ts","sourceRoot":"","sources":["../../src/utils/string-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAExC,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAA;AAC9D,wBAAgB,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,GAAG,OAAO,CAAA;AAKzD,eAAO,MAAM,IAAI,GAAa,KAAK,MAAM,KAAG,MAM3C,CAAA;AAID,eAAO,MAAM,kBAAkB,GAAa,GAAG,MAAM,KAAG,MAEvD,CAAA;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE7D;AAuBD;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,MAAM,EACnB,mBAAmB,CAAC,EAAE;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,CAAA;CAAE,EACjD,uBAAuB,CAAC,EAAE;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,CAAA;CAAE,GACpD,MAAM,CAMR"}
@@ -24,6 +24,7 @@ var __webpack_require__ = {};
24
24
  var __webpack_exports__ = {};
25
25
  __webpack_require__.r(__webpack_exports__);
26
26
  __webpack_require__.d(__webpack_exports__, {
27
+ getPersonPropertiesHash: ()=>getPersonPropertiesHash,
27
28
  includes: ()=>includes,
28
29
  isDistinctIdStringLike: ()=>isDistinctIdStringLike,
29
30
  stripLeadingDollar: ()=>stripLeadingDollar,
@@ -44,11 +45,28 @@ function isDistinctIdStringLike(value) {
44
45
  'distinctid'
45
46
  ].includes(value.toLowerCase());
46
47
  }
48
+ function deepSortKeys(value) {
49
+ if (null === value || 'object' != typeof value) return value;
50
+ if (Array.isArray(value)) return value.map(deepSortKeys);
51
+ return Object.keys(value).sort().reduce((acc, key)=>{
52
+ acc[key] = deepSortKeys(value[key]);
53
+ return acc;
54
+ }, {});
55
+ }
56
+ function getPersonPropertiesHash(distinct_id, userPropertiesToSet, userPropertiesToSetOnce) {
57
+ return JSON.stringify({
58
+ distinct_id,
59
+ userPropertiesToSet: userPropertiesToSet ? deepSortKeys(userPropertiesToSet) : void 0,
60
+ userPropertiesToSetOnce: userPropertiesToSetOnce ? deepSortKeys(userPropertiesToSetOnce) : void 0
61
+ });
62
+ }
63
+ exports.getPersonPropertiesHash = __webpack_exports__.getPersonPropertiesHash;
47
64
  exports.includes = __webpack_exports__.includes;
48
65
  exports.isDistinctIdStringLike = __webpack_exports__.isDistinctIdStringLike;
49
66
  exports.stripLeadingDollar = __webpack_exports__.stripLeadingDollar;
50
67
  exports.trim = __webpack_exports__.trim;
51
68
  for(var __webpack_i__ in __webpack_exports__)if (-1 === [
69
+ "getPersonPropertiesHash",
52
70
  "includes",
53
71
  "isDistinctIdStringLike",
54
72
  "stripLeadingDollar",
@@ -13,4 +13,19 @@ function isDistinctIdStringLike(value) {
13
13
  'distinctid'
14
14
  ].includes(value.toLowerCase());
15
15
  }
16
- export { includes, isDistinctIdStringLike, stripLeadingDollar, trim };
16
+ function deepSortKeys(value) {
17
+ if (null === value || 'object' != typeof value) return value;
18
+ if (Array.isArray(value)) return value.map(deepSortKeys);
19
+ return Object.keys(value).sort().reduce((acc, key)=>{
20
+ acc[key] = deepSortKeys(value[key]);
21
+ return acc;
22
+ }, {});
23
+ }
24
+ function getPersonPropertiesHash(distinct_id, userPropertiesToSet, userPropertiesToSetOnce) {
25
+ return JSON.stringify({
26
+ distinct_id,
27
+ userPropertiesToSet: userPropertiesToSet ? deepSortKeys(userPropertiesToSet) : void 0,
28
+ userPropertiesToSetOnce: userPropertiesToSetOnce ? deepSortKeys(userPropertiesToSetOnce) : void 0
29
+ });
30
+ }
31
+ export { getPersonPropertiesHash, includes, isDistinctIdStringLike, stripLeadingDollar, trim };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@posthog/core",
3
- "version": "1.18.0",
3
+ "version": "1.20.0",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -30,7 +30,7 @@ import {
30
30
  import { Compression, FeatureFlagError, PostHogPersistedProperty } from './types'
31
31
  import { maybeAdd, PostHogCoreStateless, QuotaLimitedFeature } from './posthog-core-stateless'
32
32
  import { uuidv7 } from './vendor/uuidv7'
33
- import { isPlainError } from './utils'
33
+ import { isEmptyObject, isNullish, isPlainError, getPersonPropertiesHash, isObject } from './utils'
34
34
 
35
35
  export abstract class PostHogCore extends PostHogCoreStateless {
36
36
  // options
@@ -47,6 +47,9 @@ export abstract class PostHogCore extends PostHogCoreStateless {
47
47
  // person profiles
48
48
  protected _personProfiles: 'always' | 'identified_only' | 'never'
49
49
 
50
+ // cache for person properties to avoid duplicate $set events
51
+ protected _cachedPersonProperties: string | null = null
52
+
50
53
  constructor(apiKey: string, options?: PostHogCoreOptions) {
51
54
  // Default for stateful mode is to not disable geoip. Only override if explicitly set
52
55
  const disableGeoipOption = options?.disableGeoip ?? false
@@ -130,6 +133,9 @@ export abstract class PostHogCore extends PostHogCoreStateless {
130
133
  // clean up props
131
134
  this.clearProps()
132
135
 
136
+ // clear cached person properties
137
+ this._cachedPersonProperties = null
138
+
133
139
  for (const key of <(keyof typeof PostHogPersistedProperty)[]>Object.keys(PostHogPersistedProperty)) {
134
140
  if (!allPropertiesToKeep.includes(PostHogPersistedProperty[key])) {
135
141
  this.setPersistedProperty((PostHogPersistedProperty as any)[key], null)
@@ -294,6 +300,10 @@ export abstract class PostHogCore extends PostHogCoreStateless {
294
300
  ...maybeAdd('$set_once', userPropsOnce),
295
301
  })
296
302
 
303
+ // Safely cast userProps and userPropsOnce to object types for hash and setPersonProperties
304
+ const userPropsObj = isObject(userProps) ? (userProps as { [key: string]: JsonType }) : undefined
305
+ const userPropsOnceObj = isObject(userPropsOnce) ? (userPropsOnce as { [key: string]: JsonType }) : undefined
306
+
297
307
  if (distinctId !== previousDistinctId) {
298
308
  // We keep the AnonymousId to be used by flags calls and identify to link the previousId
299
309
  this.setPersistedProperty(PostHogPersistedProperty.AnonymousId, previousDistinctId)
@@ -301,9 +311,16 @@ export abstract class PostHogCore extends PostHogCoreStateless {
301
311
  // Mark the user as identified
302
312
  this.setPersistedProperty(PostHogPersistedProperty.PersonMode, 'identified')
303
313
  this.reloadFeatureFlags()
304
- }
305
314
 
306
- super.identifyStateless(distinctId, allProperties, options)
315
+ super.identifyStateless(distinctId, allProperties, options)
316
+
317
+ // Update the cached person properties hash
318
+ this._cachedPersonProperties = getPersonPropertiesHash(distinctId, userPropsObj, userPropsOnceObj)
319
+ } else if (userPropsObj || userPropsOnceObj) {
320
+ // If the distinct_id is not changing, but we have user properties to set, we can check if they have changed
321
+ // and if so, send a $set event
322
+ this.setPersonProperties(userPropsObj, userPropsOnceObj)
323
+ }
307
324
  })
308
325
  }
309
326
 
@@ -429,16 +446,20 @@ export abstract class PostHogCore extends PostHogCoreStateless {
429
446
  /***
430
447
  * PROPERTIES
431
448
  ***/
432
- setPersonPropertiesForFlags(properties: { [type: string]: string }): void {
449
+ setPersonPropertiesForFlags(properties: { [type: string]: JsonType }, reloadFeatureFlags = true): void {
433
450
  this.wrap(() => {
434
451
  // Get persisted person properties
435
452
  const existingProperties =
436
- this.getPersistedProperty<Record<string, string>>(PostHogPersistedProperty.PersonProperties) || {}
453
+ this.getPersistedProperty<Record<string, JsonType>>(PostHogPersistedProperty.PersonProperties) || {}
437
454
 
438
455
  this.setPersistedProperty<PostHogEventProperties>(PostHogPersistedProperty.PersonProperties, {
439
456
  ...existingProperties,
440
457
  ...properties,
441
458
  })
459
+
460
+ if (reloadFeatureFlags) {
461
+ this.reloadFeatureFlags()
462
+ }
442
463
  })
443
464
  }
444
465
 
@@ -1162,6 +1183,79 @@ export abstract class PostHogCore extends PostHogCoreStateless {
1162
1183
  this.capture('$set', { $set: {}, $set_once: {} })
1163
1184
  }
1164
1185
 
1186
+ /**
1187
+ * Sets properties on the person profile associated with the current `distinct_id`.
1188
+ * Learn more about [identifying users](https://posthog.com/docs/product-analytics/identify)
1189
+ *
1190
+ * {@label Identification}
1191
+ *
1192
+ * @remarks
1193
+ * Updates user properties that are stored with the person profile in PostHog.
1194
+ * If `personProfiles` is set to `identified_only` and no profile exists, this will create one.
1195
+ *
1196
+ * @example
1197
+ * ```js
1198
+ * // set user properties
1199
+ * posthog.setPersonProperties({
1200
+ * email: 'user@example.com',
1201
+ * plan: 'premium'
1202
+ * })
1203
+ * ```
1204
+ *
1205
+ * @example
1206
+ * ```js
1207
+ * // set properties with $set_once
1208
+ * posthog.setPersonProperties(
1209
+ * { name: 'Max Hedgehog' }, // $set properties
1210
+ * { initial_url: '/blog' } // $set_once properties
1211
+ * )
1212
+ * ```
1213
+ *
1214
+ * @public
1215
+ *
1216
+ * @param userPropertiesToSet - Optional: An object of properties to store about the user.
1217
+ * These properties will overwrite any existing values for the same keys.
1218
+ * @param userPropertiesToSetOnce - Optional: An object of properties to store about the user.
1219
+ * If a property is previously set, this does not override that value.
1220
+ * @param reloadFeatureFlags - Whether to reload feature flags after setting the properties. Defaults to true.
1221
+ */
1222
+ setPersonProperties(
1223
+ userPropertiesToSet?: { [key: string]: JsonType },
1224
+ userPropertiesToSetOnce?: { [key: string]: JsonType },
1225
+ reloadFeatureFlags = true
1226
+ ): void {
1227
+ this.wrap(() => {
1228
+ const isSetEmpty = isNullish(userPropertiesToSet) || isEmptyObject(userPropertiesToSet)
1229
+ const isSetOnceEmpty = isNullish(userPropertiesToSetOnce) || isEmptyObject(userPropertiesToSetOnce)
1230
+ if (isSetEmpty && isSetOnceEmpty) {
1231
+ return
1232
+ }
1233
+
1234
+ if (!this._requirePersonProcessing('posthog.setPersonProperties')) {
1235
+ return
1236
+ }
1237
+
1238
+ const hash = getPersonPropertiesHash(this.getDistinctId(), userPropertiesToSet, userPropertiesToSetOnce)
1239
+
1240
+ // If exactly this $set call has been sent before, don't send it again - determine based on hash of properties
1241
+ if (this._cachedPersonProperties === hash) {
1242
+ this._logger.info(
1243
+ 'A duplicate setPersonProperties call was made with the same properties. It has been ignored.'
1244
+ )
1245
+ return
1246
+ }
1247
+
1248
+ // Update person properties for feature flags evaluation
1249
+ // Merge setOnce first, then set to allow overwriting
1250
+ const mergedProperties = { ...(userPropertiesToSetOnce || {}), ...(userPropertiesToSet || {}) }
1251
+ this.setPersonPropertiesForFlags(mergedProperties, reloadFeatureFlags)
1252
+
1253
+ this.capture('$set', { $set: userPropertiesToSet || {}, $set_once: userPropertiesToSetOnce || {} })
1254
+
1255
+ this._cachedPersonProperties = hash
1256
+ })
1257
+ }
1258
+
1165
1259
  /**
1166
1260
  * Override processBeforeEnqueue to run before_send hooks.
1167
1261
  * This runs after prepareMessage, giving users full control over the final event.
@@ -0,0 +1,121 @@
1
+ import { getPersonPropertiesHash } from './string-utils'
2
+
3
+ describe('string-utils', () => {
4
+ describe('getPersonPropertiesHash', () => {
5
+ it('should return consistent hash regardless of top-level key order', () => {
6
+ const hash1 = getPersonPropertiesHash('user-1', { b: 'value-b', a: 'value-a' })
7
+ const hash2 = getPersonPropertiesHash('user-1', { a: 'value-a', b: 'value-b' })
8
+ expect(hash1).toBe(hash2)
9
+ })
10
+
11
+ it('should return consistent hash regardless of nested object key order', () => {
12
+ const hash1 = getPersonPropertiesHash('user-1', {
13
+ nested: { z: 1, a: 2 },
14
+ })
15
+ const hash2 = getPersonPropertiesHash('user-1', {
16
+ nested: { a: 2, z: 1 },
17
+ })
18
+ expect(hash1).toBe(hash2)
19
+ })
20
+
21
+ it('should return consistent hash for deeply nested objects', () => {
22
+ const hash1 = getPersonPropertiesHash('user-1', {
23
+ level1: {
24
+ level2: {
25
+ level3: { c: 3, a: 1, b: 2 },
26
+ },
27
+ },
28
+ })
29
+ const hash2 = getPersonPropertiesHash('user-1', {
30
+ level1: {
31
+ level2: {
32
+ level3: { a: 1, b: 2, c: 3 },
33
+ },
34
+ },
35
+ })
36
+ expect(hash1).toBe(hash2)
37
+ })
38
+
39
+ it('should handle arrays with nested objects', () => {
40
+ const hash1 = getPersonPropertiesHash('user-1', {
41
+ items: [
42
+ { z: 1, a: 2 },
43
+ { y: 3, b: 4 },
44
+ ],
45
+ })
46
+ const hash2 = getPersonPropertiesHash('user-1', {
47
+ items: [
48
+ { a: 2, z: 1 },
49
+ { b: 4, y: 3 },
50
+ ],
51
+ })
52
+ expect(hash1).toBe(hash2)
53
+ })
54
+
55
+ it('should preserve array order (not sort array elements)', () => {
56
+ const hash1 = getPersonPropertiesHash('user-1', {
57
+ items: [1, 2, 3],
58
+ })
59
+ const hash2 = getPersonPropertiesHash('user-1', {
60
+ items: [3, 2, 1],
61
+ })
62
+ expect(hash1).not.toBe(hash2)
63
+ })
64
+
65
+ it('should handle null values', () => {
66
+ const hash1 = getPersonPropertiesHash('user-1', { a: null, b: 'value' })
67
+ const hash2 = getPersonPropertiesHash('user-1', { b: 'value', a: null })
68
+ expect(hash1).toBe(hash2)
69
+ })
70
+
71
+ it('should handle primitive values', () => {
72
+ const hash1 = getPersonPropertiesHash('user-1', {
73
+ str: 'string',
74
+ num: 42,
75
+ bool: true,
76
+ nil: null,
77
+ })
78
+ const hash2 = getPersonPropertiesHash('user-1', {
79
+ nil: null,
80
+ bool: true,
81
+ num: 42,
82
+ str: 'string',
83
+ })
84
+ expect(hash1).toBe(hash2)
85
+ })
86
+
87
+ it('should handle userPropertiesToSetOnce with nested objects', () => {
88
+ const hash1 = getPersonPropertiesHash('user-1', undefined, {
89
+ nested: { z: 1, a: 2 },
90
+ })
91
+ const hash2 = getPersonPropertiesHash('user-1', undefined, {
92
+ nested: { a: 2, z: 1 },
93
+ })
94
+ expect(hash1).toBe(hash2)
95
+ })
96
+
97
+ it('should handle both userPropertiesToSet and userPropertiesToSetOnce', () => {
98
+ const hash1 = getPersonPropertiesHash('user-1', { nested: { z: 1, a: 2 } }, { other: { y: 3, b: 4 } })
99
+ const hash2 = getPersonPropertiesHash('user-1', { nested: { a: 2, z: 1 } }, { other: { b: 4, y: 3 } })
100
+ expect(hash1).toBe(hash2)
101
+ })
102
+
103
+ it('should return different hash for different distinct_id', () => {
104
+ const hash1 = getPersonPropertiesHash('user-1', { a: 1 })
105
+ const hash2 = getPersonPropertiesHash('user-2', { a: 1 })
106
+ expect(hash1).not.toBe(hash2)
107
+ })
108
+
109
+ it('should return different hash for different property values', () => {
110
+ const hash1 = getPersonPropertiesHash('user-1', { a: 1 })
111
+ const hash2 = getPersonPropertiesHash('user-1', { a: 2 })
112
+ expect(hash1).not.toBe(hash2)
113
+ })
114
+
115
+ it('should handle undefined properties', () => {
116
+ const hash1 = getPersonPropertiesHash('user-1')
117
+ const hash2 = getPersonPropertiesHash('user-1', undefined, undefined)
118
+ expect(hash1).toBe(hash2)
119
+ })
120
+ })
121
+ })
@@ -1,3 +1,5 @@
1
+ import type { JsonType } from '../types'
2
+
1
3
  export function includes(str: string, needle: string): boolean
2
4
  export function includes<T>(arr: T[], needle: T): boolean
3
5
  export function includes(str: unknown[] | string, needle: unknown): boolean {
@@ -21,3 +23,41 @@ export const stripLeadingDollar = function (s: string): string {
21
23
  export function isDistinctIdStringLike(value: string): boolean {
22
24
  return ['distinct_id', 'distinctid'].includes(value.toLowerCase())
23
25
  }
26
+
27
+ /**
28
+ * Recursively sorts all keys in an object and its nested objects/arrays.
29
+ * Used to ensure deterministic JSON serialization regardless of object construction order.
30
+ */
31
+ function deepSortKeys(value: JsonType): JsonType {
32
+ if (value === null || typeof value !== 'object') {
33
+ return value
34
+ }
35
+
36
+ if (Array.isArray(value)) {
37
+ return value.map(deepSortKeys)
38
+ }
39
+
40
+ return Object.keys(value)
41
+ .sort()
42
+ .reduce((acc: { [key: string]: JsonType }, key) => {
43
+ acc[key] = deepSortKeys(value[key])
44
+ return acc
45
+ }, {})
46
+ }
47
+
48
+ /**
49
+ * Creates a hash string from distinct_id and person properties.
50
+ * Used to detect if person properties have changed to avoid duplicate $set events.
51
+ * Uses sorted keys to ensure consistent ordering regardless of object construction order.
52
+ */
53
+ export function getPersonPropertiesHash(
54
+ distinct_id: string,
55
+ userPropertiesToSet?: { [key: string]: JsonType },
56
+ userPropertiesToSetOnce?: { [key: string]: JsonType }
57
+ ): string {
58
+ return JSON.stringify({
59
+ distinct_id,
60
+ userPropertiesToSet: userPropertiesToSet ? deepSortKeys(userPropertiesToSet) : undefined,
61
+ userPropertiesToSetOnce: userPropertiesToSetOnce ? deepSortKeys(userPropertiesToSetOnce) : undefined,
62
+ })
63
+ }