@posthog/core 1.19.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 +1 -0
- package/dist/posthog-core.d.ts.map +1 -1
- package/dist/posthog-core.js +10 -3
- package/dist/posthog-core.mjs +11 -4
- 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 +32 -3
- 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;
|
|
@@ -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) {
|
|
@@ -635,6 +639,8 @@ class PostHogCore extends external_posthog_core_stateless_js_namespaceObject.Pos
|
|
|
635
639
|
const isSetOnceEmpty = (0, index_js_namespaceObject.isNullish)(userPropertiesToSetOnce) || (0, index_js_namespaceObject.isEmptyObject)(userPropertiesToSetOnce);
|
|
636
640
|
if (isSetEmpty && isSetOnceEmpty) return;
|
|
637
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.');
|
|
638
644
|
const mergedProperties = {
|
|
639
645
|
...userPropertiesToSetOnce || {},
|
|
640
646
|
...userPropertiesToSet || {}
|
|
@@ -644,6 +650,7 @@ class PostHogCore extends external_posthog_core_stateless_js_namespaceObject.Pos
|
|
|
644
650
|
$set: userPropertiesToSet || {},
|
|
645
651
|
$set_once: userPropertiesToSetOnce || {}
|
|
646
652
|
});
|
|
653
|
+
this._cachedPersonProperties = hash;
|
|
647
654
|
});
|
|
648
655
|
}
|
|
649
656
|
processBeforeEnqueue(message) {
|
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 { isEmptyObject, isNullish, 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) {
|
|
@@ -607,6 +611,8 @@ class PostHogCore extends PostHogCoreStateless {
|
|
|
607
611
|
const isSetOnceEmpty = isNullish(userPropertiesToSetOnce) || isEmptyObject(userPropertiesToSetOnce);
|
|
608
612
|
if (isSetEmpty && isSetOnceEmpty) return;
|
|
609
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.');
|
|
610
616
|
const mergedProperties = {
|
|
611
617
|
...userPropertiesToSetOnce || {},
|
|
612
618
|
...userPropertiesToSet || {}
|
|
@@ -616,6 +622,7 @@ class PostHogCore extends PostHogCoreStateless {
|
|
|
616
622
|
$set: userPropertiesToSet || {},
|
|
617
623
|
$set_once: userPropertiesToSetOnce || {}
|
|
618
624
|
});
|
|
625
|
+
this._cachedPersonProperties = hash;
|
|
619
626
|
});
|
|
620
627
|
}
|
|
621
628
|
processBeforeEnqueue(message) {
|
|
@@ -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 { isEmptyObject, isNullish, 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
|
|
|
@@ -1218,12 +1235,24 @@ export abstract class PostHogCore extends PostHogCoreStateless {
|
|
|
1218
1235
|
return
|
|
1219
1236
|
}
|
|
1220
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
|
+
|
|
1221
1248
|
// Update person properties for feature flags evaluation
|
|
1222
1249
|
// Merge setOnce first, then set to allow overwriting
|
|
1223
1250
|
const mergedProperties = { ...(userPropertiesToSetOnce || {}), ...(userPropertiesToSet || {}) }
|
|
1224
1251
|
this.setPersonPropertiesForFlags(mergedProperties, reloadFeatureFlags)
|
|
1225
1252
|
|
|
1226
1253
|
this.capture('$set', { $set: userPropertiesToSet || {}, $set_once: userPropertiesToSetOnce || {} })
|
|
1254
|
+
|
|
1255
|
+
this._cachedPersonProperties = hash
|
|
1227
1256
|
})
|
|
1228
1257
|
}
|
|
1229
1258
|
|
|
@@ -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
|
+
}
|