From 3b809f9edac9060fc78d8b0b817e0263cd3abf36 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Wed, 10 Jun 2026 13:01:43 -0500 Subject: [PATCH 1/6] Forward EventInfo.PrivTags into ext.metadata on the bond serialization path The UTC path maps EventInfo.PrivTags into ext.metadata, and JsonFormatter maps it to ext.metadata.privTags, but the cross-platform bond (CsProtocol) serializer had no ext.metadata extension, so on that path the field was only emitted as a Part C property. Add a MetaData extension (privTags) to the CsProtocol schema and route EventInfo.PrivTags into record.extMetadata in EventPropertiesDecorator, matching the UTC/JSON behavior. - CsProtocol.bond / CsProtocol_types.hpp: add MetaData { privTags } and Record.extMetadata (field 38), defined unconditionally (next to M365a) so it is present in the default build, not just HAVE_CS4_FULL. - CsProtocol_readers.hpp / CsProtocol_writers.hpp: (de)serialize the new extension. - EventPropertiesDecorator: route EventInfo.PrivTags into ext.metadata.privTags instead of emitting it as a Part C property. - tests: EventPropertiesDecoratorTests.Decorate_PrivTags_RoutedToExtMetadata. NOTE: the ext.metadata extension ordinal (38) and field layout are best-effort and must be confirmed against the canonical Common Schema wire contract before relying on them; see the TODO(privacy-parity) markers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/bond/CsProtocol.bond | 9 +++ lib/bond/generated/CsProtocol_readers.hpp | 66 +++++++++++++++++++ lib/bond/generated/CsProtocol_writers.hpp | 28 ++++++++ lib/decorators/EventPropertiesDecorator.hpp | 16 +++++ lib/include/public/CsProtocol_types.hpp | 19 ++++++ .../EventPropertiesDecoratorTests.cpp | 20 ++++++ 6 files changed, 158 insertions(+) diff --git a/lib/bond/CsProtocol.bond b/lib/bond/CsProtocol.bond index d69a287d6..4d34546e3 100644 --- a/lib/bond/CsProtocol.bond +++ b/lib/bond/CsProtocol.bond @@ -101,6 +101,13 @@ struct M365a 2: optional uint64 msp; } +struct MetaData +{ + // TODO(privacy-parity): best-effort field layout. Confirm the canonical Common + // Schema ext.metadata field ordinals/types before relying on the wire contract. + 1: optional uint64 privTags; +} + struct Xbl { 5: optional map claims; @@ -354,6 +361,8 @@ struct Record 35: optional vector extService; 36: optional vector extCs; 37: optional vector extM365a; + // TODO(privacy-parity): 38 is a best-effort ordinal for ext.metadata; confirm against canonical CS. + 38: optional vector extMetadata; 41: optional vector ext; 42: optional vector extMscv; 43: optional vector extIntWeb; diff --git a/lib/bond/generated/CsProtocol_readers.hpp b/lib/bond/generated/CsProtocol_readers.hpp index 3727b8794..d30574a97 100644 --- a/lib/bond/generated/CsProtocol_readers.hpp +++ b/lib/bond/generated/CsProtocol_readers.hpp @@ -722,6 +722,51 @@ bool Deserialize(TReader& reader, ::CsProtocol::M365a& value, bool isBase) return true; } +template +bool Deserialize(TReader& reader, ::CsProtocol::MetaData& value, bool isBase) +{ + if (!reader.ReadStructBegin(isBase)) { + return false; + } + + uint8_t type; + uint16_t id; + for (;;) { + if (!reader.ReadFieldBegin(type, id)) { + return false; + } + + if (type == BT_STOP || type == BT_STOP_BASE) { + if (isBase != (type == BT_STOP_BASE)) { + return false; + } + break; + } + + switch (id) { + case 1: { + if (!reader.ReadUInt64(value.privTags)) { + return false; + } + break; + } + + default: + return false; + } + + if (!reader.ReadFieldEnd()) { + return false; + } + } + + if (!reader.ReadStructEnd(isBase)) { + return false; + } + + return true; +} + template bool Deserialize(TReader& reader, ::CsProtocol::Xbl& value, bool isBase) { @@ -2867,6 +2912,27 @@ bool Deserialize(TReader& reader, ::CsProtocol::Record& value, bool isBase) break; } + case 38: { + uint32_t size4; + uint8_t type4; + if (!reader.ReadContainerBegin(size4, type4)) { + return false; + } + if (type4 != BT_STRUCT) { + return false; + } + value.extMetadata.resize(size4); + for (unsigned i4 = 0; i4 < size4; i4++) { + if (!Deserialize(reader, value.extMetadata[i4], false)) { + return false; + } + } + if (!reader.ReadContainerEnd()) { + return false; + } + break; + } + case 41: { uint32_t size4; uint8_t type4; diff --git a/lib/bond/generated/CsProtocol_writers.hpp b/lib/bond/generated/CsProtocol_writers.hpp index 81faf54e6..c7d33bd04 100644 --- a/lib/bond/generated/CsProtocol_writers.hpp +++ b/lib/bond/generated/CsProtocol_writers.hpp @@ -541,6 +541,22 @@ void Serialize(TWriter& writer, ::CsProtocol::M365a const& value, bool isBase) writer.WriteStructEnd(isBase); } +template +void Serialize(TWriter& writer, ::CsProtocol::MetaData const& value, bool isBase) +{ + writer.WriteStructBegin(nullptr, isBase); + + if (value.privTags != 0) { + writer.WriteFieldBegin(BT_UINT64, 1, nullptr); + writer.WriteUInt64(value.privTags); + writer.WriteFieldEnd(); + } else { + writer.WriteFieldOmitted(BT_UINT64, 1, nullptr); + } + + writer.WriteStructEnd(isBase); +} + template void Serialize(TWriter& writer, ::CsProtocol::Xbl const& value, bool isBase) { @@ -1898,6 +1914,18 @@ void Serialize(TWriter& writer, ::CsProtocol::Record const& value, bool isBase) writer.WriteFieldOmitted(BT_LIST, 37, nullptr); } + if (!value.extMetadata.empty()) { + writer.WriteFieldBegin(BT_LIST, 38, nullptr); + writer.WriteContainerBegin(value.extMetadata.size(), BT_STRUCT); + for (auto const& item2 : value.extMetadata) { + Serialize(writer, item2, false); + } + writer.WriteContainerEnd(); + writer.WriteFieldEnd(); + } else { + writer.WriteFieldOmitted(BT_LIST, 38, nullptr); + } + if (!value.ext.empty()) { writer.WriteFieldBegin(BT_LIST, 41, nullptr); writer.WriteContainerBegin(value.ext.size(), BT_STRUCT); diff --git a/lib/decorators/EventPropertiesDecorator.hpp b/lib/decorators/EventPropertiesDecorator.hpp index 5bb3e927a..556f25dcd 100644 --- a/lib/decorators/EventPropertiesDecorator.hpp +++ b/lib/decorators/EventPropertiesDecorator.hpp @@ -7,6 +7,7 @@ #include "IDecorator.hpp" #include "EventProperties.hpp" +#include "CommonFields.h" #include "CorrelationVector.hpp" #include "utils/Utils.hpp" @@ -172,6 +173,21 @@ namespace MAT_NS_BEGIN { } const auto &k = kv.first; const auto &v = kv.second; + + // Route the privacy tag (EventInfo.PrivTags) into ext.metadata.privTags so the + // cross-platform serialization path carries it the same way the UTC and JSON + // paths already do, rather than emitting it as a Part C property. + // TODO(privacy-parity): confirm the canonical Common Schema ext.metadata wire contract. + if (k == COMMONFIELDS_EVENT_PRIVTAGS) + { + if (record.extMetadata.empty()) + { + record.extMetadata.push_back(::CsProtocol::MetaData()); + } + record.extMetadata[0].privTags = static_cast(v.as_int64); + continue; + } + if (v.piiKind != PiiKind_None) { if (v.piiKind == PiiKind::CustomerContentKind_GenericData) diff --git a/lib/include/public/CsProtocol_types.hpp b/lib/include/public/CsProtocol_types.hpp index e0fcfdf5f..687fd6291 100644 --- a/lib/include/public/CsProtocol_types.hpp +++ b/lib/include/public/CsProtocol_types.hpp @@ -330,6 +330,22 @@ struct M365a { } }; +struct MetaData { + // 1: optional uint64 privTags + // TODO(privacy-parity): best-effort layout; confirm canonical Common Schema ext.metadata. + uint64_t privTags = 0; + + bool operator==(MetaData const& other) const + { + return (privTags == other.privTags); + } + + bool operator!=(MetaData const& other) const + { + return !(*this == other); + } +}; + struct Xbl { // 5: optional map claims std::map claims; @@ -1012,6 +1028,8 @@ struct Record { #endif // 37: optional vector extM365a std::vector< ::CsProtocol::M365a> extM365a; + // 38: optional vector extMetadata + std::vector< ::CsProtocol::MetaData> extMetadata; // 41: optional vector ext std::vector< ::CsProtocol::Data> ext; #ifdef HAVE_CS4_FULL @@ -1065,6 +1083,7 @@ struct Record { && (extCs == other.extCs) #endif && (extM365a == other.extM365a) + && (extMetadata == other.extMetadata) && (ext == other.ext) #ifdef HAVE_CS4_FULL && (extMscv == other.extMscv) diff --git a/tests/unittests/EventPropertiesDecoratorTests.cpp b/tests/unittests/EventPropertiesDecoratorTests.cpp index f38444fd0..3ee64d426 100644 --- a/tests/unittests/EventPropertiesDecoratorTests.cpp +++ b/tests/unittests/EventPropertiesDecoratorTests.cpp @@ -545,3 +545,23 @@ TEST(EventPropertiesDecoratorTests, DropPiiPartA_StripsValues) EXPECT_THAT(record->extSdk[0].installId, Eq("")); EXPECT_THAT(record->cV, Eq("")); } + +TEST(EventPropertiesDecoratorTests, Decorate_PrivTags_RoutedToExtMetadata) +{ + NullLogManager logManager; + EventPropertiesDecorator decorator(logManager); + Record record; + EventProperties props{"TestEvent"}; + const int64_t privTags = 0x0000000800000002; + props.SetProperty(COMMONFIELDS_EVENT_PRIVTAGS, privTags); + EventLatency latency = EventLatency::EventLatency_Normal; + + EXPECT_TRUE(decorator.decorate(record, latency, props)); + + // EventInfo.PrivTags is routed into ext.metadata.privTags ... + ASSERT_THAT(record.extMetadata, SizeIs(1)); + EXPECT_THAT(record.extMetadata[0].privTags, Eq(static_cast(privTags))); + // ... and is not emitted as a Part C property. + EXPECT_THAT(record.data[0].properties.find(COMMONFIELDS_EVENT_PRIVTAGS), + Eq(record.data[0].properties.end())); +} From 388801b25a5b7577f85339c3a71cc65eaddede4e Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Wed, 10 Jun 2026 14:00:10 -0500 Subject: [PATCH 2/6] ext-metadata: address Copilot round-1 - type guard + JSON path parity PrivTags union read (EventPropertiesDecorator.hpp): only route EventInfo.PrivTags into ext.metadata when v.type == EventProperty::TYPE_INT64; non-int64 values now fall through to normal Part C handling instead of reading the inactive union member. Enum verified at EventProperty.hpp:288 and EventProperties.cpp:318. JSON path (JsonFormatter.cpp:151): the decorator now moves privTags out of Part C into record.extMetadata, so JsonFormatter reads privTags from record.extMetadata rather than data[0].properties; otherwise the JSON path would drop ext.metadata.privTags. Added EventPropertiesDecoratorTests.Decorate_PrivTags_NonInt64_FallsThroughToPartC. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/decorators/EventPropertiesDecorator.hpp | 5 +++-- lib/system/JsonFormatter.cpp | 7 ++++--- .../EventPropertiesDecoratorTests.cpp | 18 ++++++++++++++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/lib/decorators/EventPropertiesDecorator.hpp b/lib/decorators/EventPropertiesDecorator.hpp index 556f25dcd..f3fc1c3df 100644 --- a/lib/decorators/EventPropertiesDecorator.hpp +++ b/lib/decorators/EventPropertiesDecorator.hpp @@ -176,9 +176,10 @@ namespace MAT_NS_BEGIN { // Route the privacy tag (EventInfo.PrivTags) into ext.metadata.privTags so the // cross-platform serialization path carries it the same way the UTC and JSON - // paths already do, rather than emitting it as a Part C property. + // paths already do, rather than emitting it as a Part C property. Only route a + // well-typed int64 value; anything else falls through to normal property handling. // TODO(privacy-parity): confirm the canonical Common Schema ext.metadata wire contract. - if (k == COMMONFIELDS_EVENT_PRIVTAGS) + if (k == COMMONFIELDS_EVENT_PRIVTAGS && v.type == EventProperty::TYPE_INT64) { if (record.extMetadata.empty()) { diff --git a/lib/system/JsonFormatter.cpp b/lib/system/JsonFormatter.cpp index 71b31e4e9..937c6d79c 100644 --- a/lib/system/JsonFormatter.cpp +++ b/lib/system/JsonFormatter.cpp @@ -148,9 +148,10 @@ namespace MAT_NS_BEGIN ans["iKey"] = iKey; if (!source->cV.empty()) ans[CorrelationVector::PropertyName] = source->cV; - if (source->data[0].properties.find(COMMONFIELDS_EVENT_PRIVTAGS) != source->data[0].properties.end()) { - ans["ext"]["metadata"]["privTags"] = source->data[0].properties[COMMONFIELDS_EVENT_PRIVTAGS].longValue; - source->data[0].properties.erase(COMMONFIELDS_EVENT_PRIVTAGS); + // privTags is carried in record.extMetadata (populated by EventPropertiesDecorator); + // read it from there so the JSON path matches the bond serialization path. + if (!source->extMetadata.empty() && source->extMetadata[0].privTags != 0) { + ans["ext"]["metadata"]["privTags"] = source->extMetadata[0].privTags; } addExtApp(ans, source->extApp); addExtNet(ans, source->extNet); diff --git a/tests/unittests/EventPropertiesDecoratorTests.cpp b/tests/unittests/EventPropertiesDecoratorTests.cpp index 3ee64d426..ffa5589bc 100644 --- a/tests/unittests/EventPropertiesDecoratorTests.cpp +++ b/tests/unittests/EventPropertiesDecoratorTests.cpp @@ -565,3 +565,21 @@ TEST(EventPropertiesDecoratorTests, Decorate_PrivTags_RoutedToExtMetadata) EXPECT_THAT(record.data[0].properties.find(COMMONFIELDS_EVENT_PRIVTAGS), Eq(record.data[0].properties.end())); } + +TEST(EventPropertiesDecoratorTests, Decorate_PrivTags_NonInt64_FallsThroughToPartC) +{ + NullLogManager logManager; + EventPropertiesDecorator decorator(logManager); + Record record; + EventProperties props{"TestEvent"}; + // A non-int64 PrivTags value must not be routed via the inactive union member; + // it falls through to normal Part C property handling instead. + props.SetProperty(COMMONFIELDS_EVENT_PRIVTAGS, std::string{"not-an-int"}); + EventLatency latency = EventLatency::EventLatency_Normal; + + EXPECT_TRUE(decorator.decorate(record, latency, props)); + + EXPECT_THAT(record.extMetadata, SizeIs(0)); + EXPECT_THAT(record.data[0].properties.find(COMMONFIELDS_EVENT_PRIVTAGS), + Ne(record.data[0].properties.end())); +} From d4e50a5629fc1cc3dac2f68c44a7689653d1e60b Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Wed, 10 Jun 2026 14:12:03 -0500 Subject: [PATCH 3/6] ext-metadata: address Copilot round-2 - keep privTags additive, revert JsonFormatter Round-1 moved privTags out of Part C (continue) and changed JsonFormatter to read from extMetadata, which created JSON-path edge cases (privTags lost for the non-int64 fallback and for privTags==0). Simplify: mirror EventInfo.PrivTags into ext.metadata.privTags (still guarded on TYPE_INT64) WITHOUT removing it from Part C, and revert JsonFormatter to its original Part C read. The bond path now additionally carries ext.metadata.privTags; the JSON path is unchanged from main. Updated the unit test to reflect that PrivTags is retained in Part C. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/decorators/EventPropertiesDecorator.hpp | 10 +++++----- lib/system/JsonFormatter.cpp | 7 +++---- tests/unittests/EventPropertiesDecoratorTests.cpp | 6 +++--- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/lib/decorators/EventPropertiesDecorator.hpp b/lib/decorators/EventPropertiesDecorator.hpp index f3fc1c3df..edf8d8462 100644 --- a/lib/decorators/EventPropertiesDecorator.hpp +++ b/lib/decorators/EventPropertiesDecorator.hpp @@ -174,10 +174,11 @@ namespace MAT_NS_BEGIN { const auto &k = kv.first; const auto &v = kv.second; - // Route the privacy tag (EventInfo.PrivTags) into ext.metadata.privTags so the - // cross-platform serialization path carries it the same way the UTC and JSON - // paths already do, rather than emitting it as a Part C property. Only route a - // well-typed int64 value; anything else falls through to normal property handling. + // Additionally surface the privacy tag (EventInfo.PrivTags) in ext.metadata.privTags + // so the cross-platform bond path carries it the same way the UTC path does. The + // property is intentionally left in Part C as well: the JSON path still reads it from + // there, and leaving it preserves the pre-existing behavior. Only a well-typed int64 + // value is mirrored; anything else is handled as an ordinary property below. // TODO(privacy-parity): confirm the canonical Common Schema ext.metadata wire contract. if (k == COMMONFIELDS_EVENT_PRIVTAGS && v.type == EventProperty::TYPE_INT64) { @@ -186,7 +187,6 @@ namespace MAT_NS_BEGIN { record.extMetadata.push_back(::CsProtocol::MetaData()); } record.extMetadata[0].privTags = static_cast(v.as_int64); - continue; } if (v.piiKind != PiiKind_None) diff --git a/lib/system/JsonFormatter.cpp b/lib/system/JsonFormatter.cpp index 937c6d79c..71b31e4e9 100644 --- a/lib/system/JsonFormatter.cpp +++ b/lib/system/JsonFormatter.cpp @@ -148,10 +148,9 @@ namespace MAT_NS_BEGIN ans["iKey"] = iKey; if (!source->cV.empty()) ans[CorrelationVector::PropertyName] = source->cV; - // privTags is carried in record.extMetadata (populated by EventPropertiesDecorator); - // read it from there so the JSON path matches the bond serialization path. - if (!source->extMetadata.empty() && source->extMetadata[0].privTags != 0) { - ans["ext"]["metadata"]["privTags"] = source->extMetadata[0].privTags; + if (source->data[0].properties.find(COMMONFIELDS_EVENT_PRIVTAGS) != source->data[0].properties.end()) { + ans["ext"]["metadata"]["privTags"] = source->data[0].properties[COMMONFIELDS_EVENT_PRIVTAGS].longValue; + source->data[0].properties.erase(COMMONFIELDS_EVENT_PRIVTAGS); } addExtApp(ans, source->extApp); addExtNet(ans, source->extNet); diff --git a/tests/unittests/EventPropertiesDecoratorTests.cpp b/tests/unittests/EventPropertiesDecoratorTests.cpp index ffa5589bc..df248d009 100644 --- a/tests/unittests/EventPropertiesDecoratorTests.cpp +++ b/tests/unittests/EventPropertiesDecoratorTests.cpp @@ -558,12 +558,12 @@ TEST(EventPropertiesDecoratorTests, Decorate_PrivTags_RoutedToExtMetadata) EXPECT_TRUE(decorator.decorate(record, latency, props)); - // EventInfo.PrivTags is routed into ext.metadata.privTags ... + // EventInfo.PrivTags is mirrored into ext.metadata.privTags ... ASSERT_THAT(record.extMetadata, SizeIs(1)); EXPECT_THAT(record.extMetadata[0].privTags, Eq(static_cast(privTags))); - // ... and is not emitted as a Part C property. + // ... and is also retained as a Part C property (the JSON path reads it from there). EXPECT_THAT(record.data[0].properties.find(COMMONFIELDS_EVENT_PRIVTAGS), - Eq(record.data[0].properties.end())); + Ne(record.data[0].properties.end())); } TEST(EventPropertiesDecoratorTests, Decorate_PrivTags_NonInt64_FallsThroughToPartC) From 50770ae98083a8f39ea519294c3aa88d0954b08a Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Wed, 10 Jun 2026 14:24:55 -0500 Subject: [PATCH 4/6] ext-metadata: address Copilot round-3 - single source of truth (extMetadata) Make record.extMetadata the single representation of privTags: the decorator moves a well-typed int64 EventInfo.PrivTags into ext.metadata.privTags (no longer leaving it in Part C), and JsonFormatter now emits ext.metadata.privTags from record.extMetadata whenever present. This removes the Bond-side duplication and keeps the Bond and JSON outputs aligned on one source. Non-int64 values still fall through to ordinary Part C handling. Updated the unit test accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/decorators/EventPropertiesDecorator.hpp | 11 ++++++----- lib/system/JsonFormatter.cpp | 7 ++++--- tests/unittests/EventPropertiesDecoratorTests.cpp | 6 +++--- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/decorators/EventPropertiesDecorator.hpp b/lib/decorators/EventPropertiesDecorator.hpp index edf8d8462..814462460 100644 --- a/lib/decorators/EventPropertiesDecorator.hpp +++ b/lib/decorators/EventPropertiesDecorator.hpp @@ -174,11 +174,11 @@ namespace MAT_NS_BEGIN { const auto &k = kv.first; const auto &v = kv.second; - // Additionally surface the privacy tag (EventInfo.PrivTags) in ext.metadata.privTags - // so the cross-platform bond path carries it the same way the UTC path does. The - // property is intentionally left in Part C as well: the JSON path still reads it from - // there, and leaving it preserves the pre-existing behavior. Only a well-typed int64 - // value is mirrored; anything else is handled as an ordinary property below. + // Route the privacy tag (EventInfo.PrivTags) into ext.metadata.privTags so the + // cross-platform bond path carries it the same way the UTC path does. extMetadata + // is the single source of truth: a well-typed int64 value is moved out of Part C + // (the JSON path reads it from extMetadata too). Non-int64 values are left to be + // handled as an ordinary property below. // TODO(privacy-parity): confirm the canonical Common Schema ext.metadata wire contract. if (k == COMMONFIELDS_EVENT_PRIVTAGS && v.type == EventProperty::TYPE_INT64) { @@ -187,6 +187,7 @@ namespace MAT_NS_BEGIN { record.extMetadata.push_back(::CsProtocol::MetaData()); } record.extMetadata[0].privTags = static_cast(v.as_int64); + continue; } if (v.piiKind != PiiKind_None) diff --git a/lib/system/JsonFormatter.cpp b/lib/system/JsonFormatter.cpp index 71b31e4e9..56faf1ba2 100644 --- a/lib/system/JsonFormatter.cpp +++ b/lib/system/JsonFormatter.cpp @@ -148,9 +148,10 @@ namespace MAT_NS_BEGIN ans["iKey"] = iKey; if (!source->cV.empty()) ans[CorrelationVector::PropertyName] = source->cV; - if (source->data[0].properties.find(COMMONFIELDS_EVENT_PRIVTAGS) != source->data[0].properties.end()) { - ans["ext"]["metadata"]["privTags"] = source->data[0].properties[COMMONFIELDS_EVENT_PRIVTAGS].longValue; - source->data[0].properties.erase(COMMONFIELDS_EVENT_PRIVTAGS); + // privTags is the single source of truth in record.extMetadata (populated by + // EventPropertiesDecorator for well-typed int64 values); emit it whenever present. + if (!source->extMetadata.empty()) { + ans["ext"]["metadata"]["privTags"] = source->extMetadata[0].privTags; } addExtApp(ans, source->extApp); addExtNet(ans, source->extNet); diff --git a/tests/unittests/EventPropertiesDecoratorTests.cpp b/tests/unittests/EventPropertiesDecoratorTests.cpp index df248d009..bcc8ba2a5 100644 --- a/tests/unittests/EventPropertiesDecoratorTests.cpp +++ b/tests/unittests/EventPropertiesDecoratorTests.cpp @@ -558,12 +558,12 @@ TEST(EventPropertiesDecoratorTests, Decorate_PrivTags_RoutedToExtMetadata) EXPECT_TRUE(decorator.decorate(record, latency, props)); - // EventInfo.PrivTags is mirrored into ext.metadata.privTags ... + // EventInfo.PrivTags is routed into ext.metadata.privTags (single source of truth) ... ASSERT_THAT(record.extMetadata, SizeIs(1)); EXPECT_THAT(record.extMetadata[0].privTags, Eq(static_cast(privTags))); - // ... and is also retained as a Part C property (the JSON path reads it from there). + // ... and is not also emitted as a Part C property. EXPECT_THAT(record.data[0].properties.find(COMMONFIELDS_EVENT_PRIVTAGS), - Ne(record.data[0].properties.end())); + Eq(record.data[0].properties.end())); } TEST(EventPropertiesDecoratorTests, Decorate_PrivTags_NonInt64_FallsThroughToPartC) From 36be60327c8e6f62f9d361dee7fb0c62f195de51 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Wed, 10 Jun 2026 14:38:54 -0500 Subject: [PATCH 5/6] ext-metadata: address Copilot round-4 - stop erasing PrivTags in JsonFormatter With privTags sourced from record.extMetadata, the leftover unconditional erase of COMMONFIELDS_EVENT_PRIVTAGS from Part C would drop PrivTags on the JSON path for the non-int64 fallback case (extMetadata empty, value retained as an ordinary Part C property). Remove that erase so non-int64 PrivTags is still serialized as a Part C property, while well-typed int64 values are emitted from ext.metadata. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/system/JsonFormatter.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/system/JsonFormatter.cpp b/lib/system/JsonFormatter.cpp index 56faf1ba2..47a54fa72 100644 --- a/lib/system/JsonFormatter.cpp +++ b/lib/system/JsonFormatter.cpp @@ -179,7 +179,6 @@ namespace MAT_NS_BEGIN source->data[0].properties.erase(COMMONFIELDS_APP_VERSION); source->data[0].properties.erase(COMMONFIELDS_EVENT_NAME); source->data[0].properties.erase(COMMONFIELDS_EVENT_INITID); - source->data[0].properties.erase(COMMONFIELDS_EVENT_PRIVTAGS); source->data[0].properties.erase(COMMONFIELDS_METADATA_VIEWINGPRODUCERID); source->data[0].properties.erase(COMMONFIELDS_METADATA_VIEWINGCATEGORY); source->data[0].properties.erase(COMMONFIELDS_METADATA_VIEWINGPAYLOADDECODERPATH); From 9227dfc038847ec7600d3dea225f664f81209269 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Wed, 10 Jun 2026 14:50:18 -0500 Subject: [PATCH 6/6] ext-metadata: address Copilot round-5 - clarify JsonFormatter comment Soften the "single source of truth" wording: privTags is carried in extMetadata for well-typed int64 values, while non-int64 values remain ordinary Part C properties. Comment-only change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/system/JsonFormatter.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/system/JsonFormatter.cpp b/lib/system/JsonFormatter.cpp index 47a54fa72..37f0aaf5f 100644 --- a/lib/system/JsonFormatter.cpp +++ b/lib/system/JsonFormatter.cpp @@ -148,8 +148,9 @@ namespace MAT_NS_BEGIN ans["iKey"] = iKey; if (!source->cV.empty()) ans[CorrelationVector::PropertyName] = source->cV; - // privTags is the single source of truth in record.extMetadata (populated by - // EventPropertiesDecorator for well-typed int64 values); emit it whenever present. + // For well-typed int64 values, privTags is carried in record.extMetadata (populated by + // EventPropertiesDecorator); emit it from there whenever present. Non-int64 values are left + // as ordinary Part C properties and serialized through the normal data path below. if (!source->extMetadata.empty()) { ans["ext"]["metadata"]["privTags"] = source->extMetadata[0].privTags; }