@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.
- package/dist/posthog-core.d.ts +44 -2
- package/dist/posthog-core.d.ts.map +1 -1
- package/dist/posthog-core.js +29 -4
- package/dist/posthog-core.mjs +30 -5
- package/dist/utils/string-utils.d.ts +11 -0
- package/dist/utils/string-utils.d.ts.map +1 -1
- package/dist/utils/string-utils.js +18 -0
- package/dist/utils/string-utils.mjs +16 -1
- package/package.json +1 -1
- package/src/posthog-core.ts +99 -5
- package/src/utils/string-utils.spec.ts +121 -0
- package/src/utils/string-utils.ts +40 -0
package/dist/posthog-core.d.ts
CHANGED
|
@@ -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]:
|
|
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":"AAAA,OAAO,KAAK,EACV,yBAAyB,EACzB,oBAAoB,EACpB,2BAA2B,EAC3B,kBAAkB,EAClB,sBAAsB,EACtB,qBAAqB,EACrB,QAAQ,EACR,mBAAmB,EACnB,gBAAgB,EAGhB,yBAAyB,EAKzB,sBAAsB,EAGvB,MAAM,SAAS,CAAA;AAShB,OAAO,EAAiC,wBAAwB,EAAE,MAAM,SAAS,CAAA;AACjF,OAAO,EAAY,oBAAoB,EAAuB,MAAM,0BAA0B,CAAA;AAI9F,8BAAsB,WAAY,SAAQ,oBAAoB;IAE5D,OAAO,CAAC,oBAAoB,CAAS;IACrC,OAAO,CAAC,gBAAgB,CAAiC;IACzD,OAAO,CAAC,WAAW,CAAC,CAA+B;IAGnD,SAAS,CAAC,qBAAqB,CAAC,EAAE,OAAO,CAAC,2BAA2B,GAAG,SAAS,CAAC,CAAA;IAClF,SAAS,CAAC,6BAA6B,EAAE,MAAM,CAAA;IAC/C,OAAO,CAAC,wBAAwB,CAAuB;IACvD,SAAS,CAAC,YAAY,EAAE,sBAAsB,CAAK;IAGnD,SAAS,CAAC,eAAe,EAAE,QAAQ,GAAG,iBAAiB,GAAG,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"posthog-core.d.ts","sourceRoot":"","sources":["../src/posthog-core.ts"],"names":[],"mappings":"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"}
|
package/dist/posthog-core.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/posthog-core.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
package/src/posthog-core.ts
CHANGED
|
@@ -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
|
-
|
|
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]:
|
|
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,
|
|
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
|
+
}
|