fix(attributes): preserve null custom attributes and iOS New Arch product attributes#362
Conversation
…duct attributes Normalize explicit null custom-attribute values to "" before serialization so they are preserved consistently in Live Stream on iOS and Android, and fix iOS New Architecture product conversion to retain product-level custom attributes. - JS: non-mutating null -> "" normalization at the bridge boundary for event/screen/MP/commerce events and product/impression attributes; widen public CustomAttributes type to allow null. - iOS: RNMParticleStringAttributes (products: stringify + null->"") and RNMParticleEventAttributes (events: preserve value types, null->"" only); apply product customAttributes in New Architecture createMPProductFromDict. - Android: unit coverage for existing null -> "" bridge conversion. - Docs: README + CHANGELOG guidance on null custom attributes. - Tests: JS jest suite, iOS XCTest coverage, Android unit test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PR SummaryMedium Risk Overview iOS: New Architecture product mapping applies Tests & tooling: Jest ( Reviewed by Cursor Bugbot for commit 9b4d42c. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Pull request overview
This PR standardizes how null custom attributes are handled at the React Native bridge boundary (normalizing explicit null to empty string), and fixes an iOS New Architecture issue where product-level customAttributes were being dropped. It also adds tests across JS/iOS/Android plus documentation updates to clarify the intended behavior.
Changes:
- JS: add non-mutating normalization helpers to convert explicit
nullattribute values to''before crossing the bridge; widen publicCustomAttributesto allownull. - iOS: add attribute-normalization helpers for event-level vs product-level attributes, and apply product
customAttributesin the New Architecture product mapping. - Tests/docs/tooling: add JS Jest tests + config, add iOS commerce mapping tests, add Android unit test, and document the
nullbehavior in README/CHANGELOG.
Reviewed changes
Copilot reviewed 8 out of 10 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| yarn.lock | Adds/updates Jest + ts-jest and related dependency lock entries. |
| package.json | Runs Jest in npm test and adds Jest/ts-jest typings & deps. |
| jest.config.js | Introduces ts-jest configuration to run JS/TS unit tests under js/**. |
| js/index.tsx | Adds normalize* helpers and normalizes null attributes at the JS→native boundary. |
| js/tests/attribute-normalization.test.ts | Adds Jest coverage for null-to-empty-string normalization and non-mutation guarantees. |
| ios/RNMParticle/RNMParticle.mm | Normalizes event-level attributes (preserve types; null→"") and product attributes (string-only; null→""), and applies product customAttributes in New Arch mapping. |
| sample/ios/MParticleSampleTests/RCTConvertCommerceMappingTests.m | Adds regression tests covering legacy + New Arch product attrs and legacy event/commerce null normalization. |
| android/src/test/java/com/mparticle/react/MParticleModuleTest.java | Adds unit test to lock in Android null→"" custom attribute conversion. |
| README.md | Documents intended handling of explicit null custom attribute values. |
| CHANGELOG.md | Notes the cross-platform null-handling change and iOS New Arch product fix. |
Comments suppressed due to low confidence (4)
js/index.tsx:182
- normalizeMPEvent only checks info === undefined; if event.info is null at runtime (common from plain JS or any-casts), normalizeCustomAttributes(null) will throw. Consider treating null the same as undefined (omit info) to avoid a crash.
const { info, ...eventProperties } = event;
if (info === undefined) {
return eventProperties;
js/index.tsx:202
- normalizeCommerceEvent treats customAttributes as present when it is null (only checks === undefined), which will crash in normalizeCustomAttributes(null). Handle null the same as undefined by omitting the customAttributes field.
...(customAttributes === undefined
? {}
: {
customAttributes: normalizeCustomAttributes(customAttributes),
}),
js/index.tsx:205
- normalizeCommerceEvent only checks products === undefined before calling products.map(...). If products is null at runtime, this will throw. Handle null the same as undefined (omit products).
...(products === undefined
? {}
: { products: products.map(normalizeProduct) }),
js/index.tsx:208
- normalizeCommerceEvent only checks impressions === undefined before calling impressions.map(...). If impressions is null at runtime, this will throw. Handle null the same as undefined (omit impressions).
...(impressions === undefined
? {}
: { impressions: impressions.map(normalizeImpression) }),
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…e containers - podspec: mParticle-Apple-SDK-ObjC floor '~> 9.2' -> '>= 9.2.2', '< 10.0'. The device-consent bridge uses MParticle.deviceConsentState, introduced in SDK 9.2.2; the old floor permitted 9.2.0/9.2.1 which fail to compile. - js: treat a null customAttributes/info/products/impressions container the same as undefined (omit it) instead of calling Object.keys(null)/null.map, which threw for plain-JS consumers. Addresses PR review feedback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 9b4d42c. Configure here.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 11 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
js/index.tsx:239
- logScreenEvent currently checks only
attributes === null. If a consumer callslogScreenEvent(screenName, undefined, shouldUploadEvent), this will attemptnormalizeCustomAttributes(undefined)and throw. Treatundefinedthe same asnullhere.
MParticleModule.logScreenEvent(
screenName,
attributes === null ? null : normalizeCustomAttributes(attributes),
shouldUploadEvent
);
BrandonStalnaker
left a comment
There was a problem hiding this comment.
A nice to add but otherwise good

Context
Two React Native SDK defects were reported:
nullcustom event attributes behaved inconsistently across platforms.This PR unifies null handling and fixes the New Architecture product bug.
Behavior
nullcustom attribute →""on both platforms (event, commerce, and product level).{ coupon_code: null }shows as{ coupon_code: "" }in Live Stream. Omit the key to represent an absent value.customAttributes.42stays42), Android continues to stringify. Onlynullhandling is unified.Changes
JS (
js/index.tsx)normalize*helpers convertnull→''at the bridge boundary forlogEvent,logScreenEvent,logMPEvent,logCommerceEvent(event + product + impression attributes).nullattribute container (e.g.product.customAttributes = null,event.info = null) is treated the same asundefined(field omitted) instead of hittingObject.keys(null)/null.map— prevents a runtime crash for plain-JS consumers. (PR review feedback.)CustomAttributestype widened to allownull; the Codegen spec type stays non-null since values are normalized before crossing the bridge.iOS (
ios/RNMParticle/RNMParticle.mm)RNMParticleStringAttributes(products): stringify values +null→""— required byMPProduct's string-only custom-attribute API.RNMParticleEventAttributes(events/commerce-event level): preserve value types,null→""only.createMPProductFromDictnow appliescustomAttributes(the dropped-attribute fix).RCTConvertpaths aligned with the same helpers.Dependency floor (
react-native-mparticle.podspec)mParticle-Apple-SDK-ObjCfloor raised~> 9.2→>= 9.2.2, < 10.0. The device-consent bridge (added in feat: expose device-based consent APIs in React Native bridge #351) usesMParticle.deviceConsentState, introduced in SDK 9.2.2; the old~> 9.2floor permitted9.2.0/9.2.1, which lack the property and fail to compile.Android — no production change (
convertStringMapalready didnull→""); addedMParticleModuleTestto lock it in.Docs — README + CHANGELOG guidance on null custom attributes.
Tests
js/__tests__/attribute-normalization.test.ts— null→''normalization, input-non-mutation across all paths, and null-container omission. ✅ 5/5 pass.RCTConvertCommerceMappingTests— New Arch + legacy product attrs (string/number/bool/null), legacy event/commerce null→"", and event-level numeric preserved.MParticleModuleTest— null event attr →"".npx tsc --noEmitandeslint✅ clean.Verification notes
yarn jest(5 tests),npx tsc --noEmit, andeslintpass locally.property 'deviceConsentState' not found) was a dependency-floor mismatch, now fixed: the podspec floor is raised to>= 9.2.2, anddeviceConsentStateships in SDK 9.2.2 (pod installresolves to 9.3.x, which has it). Becausesample/ios/Podfile.lockis gitignored, runpod install --repo-updateinsample/iosto re-resolve the stale9.2.1lock before building the iOS test target.🤖 Generated with Claude Code