From 0492a6597feae153b50ae6488888b0de2473ab0c Mon Sep 17 00:00:00 2001 From: Ayush Jhanwar Date: Wed, 2 Sep 2026 13:29:06 +0530 Subject: [PATCH] fix(track): don't drop increment/decrement for a not-yet-created profile adjustProfileProperty threw HttpError 404 ('Profile not found') when getProfileById returned nothing, and 400 when the existing value wasn't numeric. Both cases silently drop the operation. A missing profile is not an error condition here: an increment/decrement routinely fires before the profile's first event or identify has landed (a race), or for an anonymous/transient id. Rejecting it loses the delta and never creates the counter. Adopt Mixpanel $add / $subtract semantics: treat the current value as 0, upsert (creating the profile if needed), and apply the delta. A non-numeric existing value is skipped rather than 400'd, and is never overwritten with NaN. Existing numeric profiles are unaffected. --- apps/api/src/controllers/track.controller.ts | 30 +++++++++++--------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/apps/api/src/controllers/track.controller.ts b/apps/api/src/controllers/track.controller.ts index 3452de4f8..55efe6bae 100644 --- a/apps/api/src/controllers/track.controller.ts +++ b/apps/api/src/controllers/track.controller.ts @@ -321,29 +321,33 @@ async function adjustProfileProperty( ): Promise { const { profileId, property, value } = payload; const profile = await getProfileById(String(profileId), projectId); - if (!profile) { - throw new HttpError('Profile not found', { status: 404 }); - } + + // A missing profile is not an error. An increment/decrement legitimately + // fires before the profile's first event or identify has landed (a common + // race), or for an anonymous/transient id. Follow Mixpanel `$add`/`$subtract` + // semantics — treat the current value as 0 and upsert — so the delta is + // preserved and the profile is created, instead of dropping the operation. + const properties = profile?.properties ?? {}; const parsed = Number.parseInt( - pathOr('0', property.split('.'), profile.properties), + pathOr('0', property.split('.'), properties), 10 ); + // An existing value that isn't a number can't be adjusted; skip rather than + // rejecting the request (and never overwrite it with a NaN). if (Number.isNaN(parsed)) { - throw new HttpError('Property value is not a number', { status: 400 }); + return; } - profile.properties = assocPath( - property.split('.'), - parsed + direction * (value || 1), - profile.properties - ); - await upsertProfile({ - id: profile.id, + id: profile?.id ?? String(profileId), projectId, - properties: profile.properties, + properties: assocPath( + property.split('.'), + parsed + direction * (value || 1), + properties + ), isExternal: true, }); }