Repository: honeycombio/refinery
Pull request #1611 (2025-07-03 01:27:09)

fix: fully initialize Payload object during unmarshaling
## Which problem is this PR solving?

Currently, the Payload object isn’t fully initialized until ExtractMetadata is explicitly called. This creates a risk: if a developer forgets to call ExtractMetadata, the Payload instance may appear valid but lack critical metadata — leading to subtle bugs.

## Short description of the changes

- Modified Payload to hold a reference to the Config object.
- Enabled automatic metadata extraction during unmarshaling by invoking ExtractMetadata within the unmarshal logic.
- Standardized Payload initialization through NewPayload and NewEmptyPayload constructors.
- Applied the same initialization logic for JSON format.
- Exception: OTEL data deserialization happens in Husky, so ExtractMetadata must still be called manually after Husky returns parsed results.

While reviewing the transmission logic, I found two metadata fields that shouldn’t always be forwarded to Honeycomb:
- meta.trace_id: Only used in distributed mode. Now replaced with the configured trace ID field name for decision spans, ensuring meta.trace_id is never sent externally.
- meta.refinery.root: Also used only for internal peer communication. Updated logic to only send this field if its value is true.




Patch:
diff --git a/collect/collect.go b/collect/collect.go
index 3062107..b6f0fa9 100644
--- a/collect/collect.go
+++ b/collect/collect.go
@@ -1316,7 +1316,7 @@ func (i *InMemCollector) createDecisionSpan(sp *types.Span, trace *types.Trace,
 		i.datasetSamplers[selector] = sampler
 	}
 
-	dc := sp.ExtractDecisionContext()
+	dc := sp.ExtractDecisionContext(i.Config)
 	// extract all key fields from the span
 	keyFields, nonRootFields := sampler.GetKeyFields()
 	if sp.IsRoot {
diff --git a/route/batched_event.go b/route/batched_event.go
index 939c71d..b9371e1 100644
--- a/route/batched_event.go
+++ b/route/batched_event.go
@@ -2,8 +2,10 @@ package route
 
 import (
 	"bytes"
+	"encoding/json"
 	"time"
 
+	"github.com/honeycombio/refinery/config"
 	"github.com/honeycombio/refinery/types"
 	"github.com/tinylib/msgp/msgp"
 )
@@ -13,6 +15,7 @@ type batchedEvent struct {
 	MsgPackTimestamp *time.Time    `msgpack:"time,omitempty"`
 	SampleRate       int64         `json:"samplerate" msgpack:"samplerate"`
 	Data             types.Payload `json:"data" msgpack:"data"`
+	cfg              config.Config `json:"-" msgpack:"-"`
 }
 
 func (b *batchedEvent) getEventTime() time.Time {
@@ -30,6 +33,33 @@ func (b *batchedEvent) getSampleRate() uint {
 	return uint(b.SampleRate)
 }
 
+func (b *batchedEvent) UnmarshalJSON(data []byte) error {
+	type tempEvent struct {
+		Timestamp  string          `json:"time"`
+		SampleRate int64           `json:"samplerate"`
+		Data       json.RawMessage `json:"data"`
+	}
+
+	var temp tempEvent
+	err := json.Unmarshal(data, &temp)
+	if err != nil {
+		return err
+	}
+
+	// Copy the simple fields
+	b.Timestamp = temp.Timestamp
+	b.SampleRate = temp.SampleRate
+
+	// Initialize Data with config and then unmarshal the raw JSON into it
+	b.Data = types.NewPayload(b.cfg, nil)
+	err = json.Unmarshal(temp.Data, &b.Data)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
 // UnmarshalMsg implements msgp.Unmarshaler
 // Based on generated code from the msgp tool, but modified for clarity and to
 // avoid the unnecessary unsafe string conversion.
@@ -54,9 +84,6 @@ func (b *batchedEvent) UnmarshalMsg(bts []byte) (o []byte, err error) {
 		case bytes.Equal(field, []byte("time")):
 			if msgp.IsNil(bts) {
 				bts, err = msgp.ReadNilBytes(bts)
-				if err != nil {
-					return
-				}
 				b.MsgPackTimestamp = nil
 			} else {
 				if b.MsgPackTimestamp == nil {
@@ -75,6 +102,7 @@ func (b *batchedEvent) UnmarshalMsg(bts []byte) (o []byte, err error) {
 				return
 			}
 		case bytes.Equal(field, []byte("data")):
+			b.Data = types.NewPayload(b.cfg, nil)
 			bts, err = b.Data.UnmarshalMsg(bts)
 			if err != nil {
 				err = msgp.WrapError(err, "Data")
@@ -93,7 +121,17 @@ func (b *batchedEvent) UnmarshalMsg(bts []byte) (o []byte, err error) {
 }
 
 // Create a type for []batchedEvent so we can give it an unmarshaler.
-type batchedEvents []batchedEvent
+type batchedEvents struct {
+	events []batchedEvent
+	cfg    config.Config
+}
+
+func newBatchedEvents(cfg config.Config) *batchedEvents {
+	return &batchedEvents{
+		events: make([]batchedEvent, 0),
+		cfg:    cfg,
+	}
+}
 
 // UnmarshalMsg implements msgp.Unmarshaler
 func (b *batchedEvents) UnmarshalMsg(bts []byte) (o []byte, err error) {
@@ -103,13 +141,10 @@ func (b *batchedEvents) UnmarshalMsg(bts []byte) (o []byte, err error) {
 		err = msgp.WrapError(err)
 		return
 	}
-	if cap(*b) >= int(totalValues) {
-		*b = (*b)[:totalValues]
-	} else {
-		*b = make(batchedEvents, totalValues)
-	}
-	for i := range *b {
-		bts, err = (*b)[i].UnmarshalMsg(bts)
+	b.events = make([]batchedEvent, totalValues)
+	for i := range b.events {
+		b.events[i].cfg = b.cfg
+		bts, err = b.events[i].UnmarshalMsg(bts)
 		if err != nil {
 			err = msgp.WrapError(err, i)
 			return
@@ -119,85 +154,26 @@ func (b *batchedEvents) UnmarshalMsg(bts []byte) (o []byte, err error) {
 	return
 }
 
-// UnmarshalMsgWithMetadata is an optimized version that extracts metadata during unmarshaling
-func (b *batchedEvents) UnmarshalMsgWithMetadata(bts []byte, traceIdFieldNames, parentIdFieldNames []string) (o []byte, err error) {
-	var totalValues uint32
-	totalValues, bts, err = msgp.ReadArrayHeaderBytes(bts)
-	if err != nil {
-		err = msgp.WrapError(err)
-		return
-	}
-	if cap(*b) >= int(totalValues) {
-		*b = (*b)[:totalValues]
-	} else {
-		*b = make(batchedEvents, totalValues)
-	}
-	for i := range *b {
-		bts, err = (*b)[i].UnmarshalMsgWithMetadata(bts, traceIdFieldNames, parentIdFieldNames)
-		if err != nil {
-			err = msgp.WrapError(err, i)
-			return
-		}
-	}
-	o = bts
-	return
+func (b *batchedEvents) MarshalJSON() ([]byte, error) {
+	return json.Marshal(b.events)
 }
 
-// UnmarshalMsgWithMetadata is an optimized version that unmarshals and extracts metadata in one pass
-func (b *batchedEvent) UnmarshalMsgWithMetadata(bts []byte, traceIdFieldNames, parentIdFieldNames []string) (o []byte, err error) {
-	var field []byte
-	var fieldsRemaining uint32
-	fieldsRemaining, bts, err = msgp.ReadMapHeaderBytes(bts)
+func (b *batchedEvents) UnmarshalJSON(data []byte) error {
+	var rawEvents []json.RawMessage
+	err := json.Unmarshal(data, &rawEvents)
 	if err != nil {
-		err = msgp.WrapError(err)
-		return
+		return err
 	}
-	for fieldsRemaining > 0 {
-		fieldsRemaining--
-		field, bts, err = msgp.ReadMapKeyZC(bts)
+
+	b.events = make([]batchedEvent, len(rawEvents))
+	for i := range b.events {
+		b.events[i].cfg = b.cfg
+
+		err = json.Unmarshal(rawEvents[i], &b.events[i])
 		if err != nil {
-			err = msgp.WrapError(err)
-			return
-		}
-		switch {
-		case bytes.Equal(field, []byte("time")):
-			if msgp.IsNil(bts) {
-				bts, err = msgp.ReadNilBytes(bts)
-				if err != nil {
-					return
-				}
-				b.MsgPackTimestamp = nil
-			} else {
-				if b.MsgPackTimestamp == nil {
-					b.MsgPackTimestamp = new(time.Time)
-				}
-				*b.MsgPackTimestamp, bts, err = msgp.ReadTimeBytes(bts)
-				if err != nil {
-					err = msgp.WrapError(err, "MsgPackTimestamp")
-					return
-				}
-			}
-		case bytes.Equal(field, []byte("samplerate")):
-			b.SampleRate, bts, err = msgp.ReadInt64Bytes(bts)
-			if err != nil {
-				err = msgp.WrapError(err, "SampleRate")
-				return
-			}
-		case bytes.Equal(field, []byte("data")):
-			// Use the optimized UnmarshalMsgWithMetadata for the payload
-			bts, err = b.Data.UnmarshalMsgWithMetadata(bts, traceIdFieldNames, parentIdFieldNames)
-			if err != nil {
-				err = msgp.WrapError(err, "Data")
-				return
-			}
-		default:
-			bts, err = msgp.Skip(bts)
-			if err != nil {
-				err = msgp.WrapError(err)
-				return
-			}
+			return err
 		}
 	}
-	o = bts
-	return
+
+	return nil
 }
diff --git a/route/route.go b/route/route.go
index 1d45a20..b7e8f53 100644
--- a/route/route.go
+++ b/route/route.go
@@ -453,7 +453,7 @@ func (r *Router) requestToEvent(ctx context.Context, req *http.Request, reqBod [
 		Environment: environment,
 		SampleRate:  uint(sampleRate),
 		Timestamp:   eventTime,
-		Data:        types.NewPayload(data),
+		Data:        types.NewPayload(r.Config, data),
 	}, nil
 }
 
@@ -477,24 +477,16 @@ func (r *Router) batch(w http.ResponseWriter, req *http.Request) {
 		return
 	}
 
-	batchedEvents := make(batchedEvents, 0)
+	batchedEvents := newBatchedEvents(r.Config)
 
-	// Use optimized unmarshal for msgpack that extracts metadata in one pass
-	contentType := req.Header.Get("Content-Type")
-	if contentType == "application/x-msgpack" || contentType == "application/msgpack" {
-		traceIdFieldNames := r.Config.GetTraceIdFieldNames()
-		parentIdFieldNames := r.Config.GetParentIdFieldNames()
-		_, err = batchedEvents.UnmarshalMsgWithMetadata(reqBod, traceIdFieldNames, parentIdFieldNames)
-	} else {
-		err = unmarshal(req, reqBod, &batchedEvents)
-	}
+	err = unmarshal(req, reqBod, batchedEvents)
 
 	if err != nil {
 		debugLog.WithField("error", err.Error()).WithField("request.url", req.URL).WithField("json_body", string(reqBod)).Logf("error parsing json")
 		r.handlerReturnWithError(w, ErrJSONFailed, err)
 		return
 	}
-	r.Metrics.Count(r.incomingOrPeer+"_router_batch_events", int64(len(batchedEvents)))
+	r.Metrics.Count(r.incomingOrPeer+"_router_batch_events", int64(len(batchedEvents.events)))
 
 	dataset, err := getDatasetFromRequest(req)
 	if err != nil {
@@ -514,8 +506,8 @@ func (r *Router) batch(w http.ResponseWriter, req *http.Request) {
 	}
 
 	userAgent := getUserAgentFromRequest(req)
-	batchedResponses := make([]*BatchResponse, 0, len(batchedEvents))
-	for _, bev := range batchedEvents {
+	batchedResponses := make([]*BatchResponse, 0, len(batchedEvents.events))
+	for _, bev := range batchedEvents.events {
 		ev := &types.Event{
 			Context:     ctx,
 			APIHost:     apiHost,
@@ -581,7 +573,7 @@ func (router *Router) processOTLPRequest(
 				Environment: environment,
 				SampleRate:  uint(ev.SampleRate),
 				Timestamp:   ev.Timestamp,
-				Data:        types.NewPayload(ev.Attributes),
+				Data:        types.NewPayload(router.Config, ev.Attributes),
 			}
 			addIncomingUserAgent(event, incomingUserAgent)
 			if err = router.processEvent(event, requestID); err != nil {
@@ -606,13 +598,12 @@ func (r *Router) processEvent(ev *types.Event, reqID interface{}) error {
 	r.Metrics.Histogram(r.incomingOrPeer+"_router_event_bytes", float64(ev.GetDataSize()))
 
 	// An error here is effectively a parsing error, so return it up the stack.
-	err := ev.Data.ExtractMetadata(r.Config.GetTraceIdFieldNames(), r.Config.GetParentIdFieldNames())
-	if err != nil {
+	if err := ev.Data.ExtractMetadata(); err != nil {
 		return err
 	}
 
 	// check if this is a probe from another refinery; if so, we should drop it
-	if ev.Data.MetaRefineryProbe.Value {
+	if ev.Data.MetaRefineryProbe.HasValue && ev.Data.MetaRefineryProbe.Value {
 		debugLog.Logf("dropping probe")
 		return nil
 	}
@@ -692,6 +683,7 @@ func (r *Router) processEvent(ev *types.Event, reqID interface{}) error {
 		return nil
 	}
 
+	var err error
 	// we're supposed to handle it normally
 	if r.incomingOrPeer == "incoming" {
 		err = r.Collector.AddSpan(span)
diff --git a/types/event.go b/types/event.go
index 08f2bbe..353ac87 100644
--- a/types/event.go
+++ b/types/event.go
@@ -6,6 +6,7 @@ import (
 	"time"
 
 	huskyotlp "github.com/honeycombio/husky/otlp"
+	"github.com/honeycombio/refinery/config"
 )
 
 const (
@@ -245,13 +246,14 @@ func (sp *Span) IsDecisionSpan() bool {
 
 // ExtractDecisionContext returns a new Event that contains only the data that is
 // relevant to the decision-making process.
-func (sp *Span) ExtractDecisionContext() *Event {
+func (sp *Span) ExtractDecisionContext(config config.Config) *Event {
 	decisionCtx := sp.Event
 	dataSize := sp.Event.GetDataSize()
 
 	// Create a new empty payload and set metadata fields directly
-	decisionCtx.Data = NewPayload(map[string]interface{}{})
-	decisionCtx.Data.MetaTraceID = sp.TraceID
+	decisionCtx.Data = NewPayload(config, nil)
+	// use the configured trace ID field name to set the trace ID
+	decisionCtx.Data.Set(config.GetTraceIdFieldNames()[0], sp.TraceID)
 	decisionCtx.Data.MetaRefineryRoot.Set(sp.IsRoot)
 	decisionCtx.Data.MetaRefineryMinSpan.Set(true)
 	decisionCtx.Data.MetaAnnotationType = sp.AnnotationType().String()
diff --git a/types/payload.go b/types/payload.go
index 2d38366..4c977f5 100644
--- a/types/payload.go
+++ b/types/payload.go
@@ -9,6 +9,7 @@ import (
 	"slices"
 	"strings"
 
+	"github.com/honeycombio/refinery/config"
 	jsoniter "github.com/json-iterator/go"
 	"github.com/tinylib/msgp/msgp"
 )
@@ -69,6 +70,7 @@ type metadataField struct {
 	expectedType  FieldType
 	get           func(p *Payload) (value any, ok bool)               // Payload.Get, Payload.All
 	set           func(p *Payload, value any)                         // Payload.Set
+	exist         func(p *Payload) bool                               // Payload.Exists
 	appendMsgp    func(p *Payload, in []byte) (out []byte, ok bool)   // Payload.MarshalMsg
 	unmarshalMsgp func(p *Payload, in []byte) (out []byte, err error) // Payload.extractMetadataFromBytes
 }
@@ -88,6 +90,9 @@ var metadataFields = []metadataField{
 				p.MetaSignalType = v
 			}
 		},
+		exist: func(p *Payload) bool {
+			return p.MetaSignalType != ""
+		},
 		appendMsgp: func(p *Payload, in []byte) (out []byte, ok bool) {
 			if p.MetaSignalType != "" {
 				out = msgp.AppendString(in, MetaSignalType)
@@ -115,12 +120,10 @@ var metadataFields = []metadataField{
 				p.MetaTraceID = v
 			}
 		},
+		exist: func(p *Payload) bool {
+			return p.MetaTraceID != ""
+		},
 		appendMsgp: func(p *Payload, in []byte) (out []byte, ok bool) {
-			if p.MetaTraceID != "" {
-				out = msgp.AppendString(in, MetaTraceID)
-				out = msgp.AppendString(out, p.MetaTraceID)
-				return out, true
-			}
 			return in, false
 		},
 		unmarshalMsgp: func(p *Payload, in []byte) (out []byte, err error) {
@@ -142,6 +145,9 @@ var metadataFields = []metadataField{
 				p.MetaAnnotationType = v
 			}
 		},
+		exist: func(p *Payload) bool {
+			return p.MetaAnnotationType != ""
+		},
 		appendMsgp: func(p *Payload, in []byte) (out []byte, ok bool) {
 			if p.MetaAnnotationType != "" {
 				out = msgp.AppendString(in, MetaAnnotationType)
@@ -169,6 +175,9 @@ var metadataFields = []metadataField{
 				p.MetaRefineryProbe.Set(v)
 			}
 		},
+		exist: func(p *Payload) bool {
+			return p.MetaRefineryProbe.HasValue
+		},
 		appendMsgp: func(p *Payload, in []byte) (out []byte, ok bool) {
 			if p.MetaRefineryProbe.HasValue {
 				out = msgp.AppendString(in, MetaRefineryProbe)
@@ -200,8 +209,11 @@ var metadataFields = []metadataField{
 				p.MetaRefineryRoot.Set(v)
 			}
 		},
+		exist: func(p *Payload) bool {
+			return p.MetaRefineryRoot.HasValue
+		},
 		appendMsgp: func(p *Payload, in []byte) (out []byte, ok bool) {
-			if p.MetaRefineryRoot.HasValue {
+			if p.MetaRefineryRoot.HasValue && p.MetaRefineryRoot.Value {
 				out = msgp.AppendString(in, MetaRefineryRoot)
 				out = msgp.AppendBool(out, p.MetaRefineryRoot.Value)
 				return out, true
@@ -231,6 +243,9 @@ var metadataFields = []metadataField{
 				p.MetaRefineryIncomingUserAgent = v
 			}
 		},
+		exist: func(p *Payload) bool {
+			return p.MetaRefineryIncomingUserAgent != ""
+		},
 		appendMsgp: func(p *Payload, in []byte) (out []byte, ok bool) {
 			if p.MetaRefineryIncomingUserAgent != "" {
 				out = msgp.AppendString(in, MetaRefineryIncomingUserAgent)
@@ -258,6 +273,9 @@ var metadataFields = []metadataField{
 				p.MetaRefinerySendBy = v
 			}
 		},
+		exist: func(p *Payload) bool {
+			return p.MetaRefinerySendBy != 0
+		},
 		appendMsgp: func(p *Payload, in []byte) (out []byte, ok bool) {
 			if p.MetaRefinerySendBy != 0 {
 				out = msgp.AppendString(in, MetaRefinerySendBy)
@@ -285,6 +303,9 @@ var metadataFields = []metadataField{
 				p.MetaRefinerySpanDataSize = v
 			}
 		},
+		exist: func(p *Payload) bool {
+			return p.MetaRefinerySpanDataSize != 0
+		},
 		appendMsgp: func(p *Payload, in []byte) (out []byte, ok bool) {
 			if p.MetaRefinerySpanDataSize != 0 {
 				out = msgp.AppendString(in, MetaRefinerySpanDataSize)
@@ -312,6 +333,9 @@ var metadataFields = []metadataField{
 				p.MetaRefineryMinSpan.Set(v)
 			}
 		},
+		exist: func(p *Payload) bool {
+			return p.MetaRefineryMinSpan.HasValue
+		},
 		appendMsgp: func(p *Payload, in []byte) (out []byte, ok bool) {
 			if p.MetaRefineryMinSpan.HasValue {
 				out = msgp.AppendString(in, MetaRefineryMinSpan)
@@ -343,6 +367,9 @@ var metadataFields = []metadataField{
 				p.MetaRefineryForwarded = v
 			}
 		},
+		exist: func(p *Payload) bool {
+			return p.MetaRefineryForwarded != ""
+		},
 		appendMsgp: func(p *Payload, in []byte) (out []byte, ok bool) {
 			if p.MetaRefineryForwarded != "" {
 				out = msgp.AppendString(in, MetaRefineryForwarded)
@@ -370,6 +397,9 @@ var metadataFields = []metadataField{
 				p.MetaRefineryExpiredTrace.Set(v)
 			}
 		},
+		exist: func(p *Payload) bool {
+			return p.MetaRefineryExpiredTrace.HasValue
+		},
 		appendMsgp: func(p *Payload, in []byte) (out []byte, ok bool) {
 			if p.MetaRefineryExpiredTrace.HasValue {
 				out = msgp.AppendString(in, MetaRefineryExpiredTrace)
@@ -401,6 +431,9 @@ var metadataFields = []metadataField{
 				p.MetaRefineryLocalHostname = v
 			}
 		},
+		exist: func(p *Payload) bool {
+			return p.MetaRefineryLocalHostname != ""
+		},
 		appendMsgp: func(p *Payload, in []byte) (out []byte, ok bool) {
 			if p.MetaRefineryLocalHostname != "" {
 				out = msgp.AppendString(in, MetaRefineryLocalHostname)
@@ -428,6 +461,9 @@ var metadataFields = []metadataField{
 				p.MetaStressed.Set(v)
 			}
 		},
+		exist: func(p *Payload) bool {
+			return p.MetaStressed.HasValue
+		},
 		appendMsgp: func(p *Payload, in []byte) (out []byte, ok bool) {
 			if p.MetaStressed.HasValue {
 				out = msgp.AppendString(in, MetaStressed)
@@ -459,6 +495,9 @@ var metadataFields = []metadataField{
 				p.MetaRefineryReason = v
 			}
 		},
+		exist: func(p *Payload) bool {
+			return p.MetaRefineryReason != ""
+		},
 		appendMsgp: func(p *Payload, in []byte) (out []byte, ok bool) {
 			if p.MetaRefineryReason != "" {
 				out = msgp.AppendString(in, MetaRefineryReason)
@@ -486,6 +525,9 @@ var metadataFields = []metadataField{
 				p.MetaRefinerySendReason = v
 			}
 		},
+		exist: func(p *Payload) bool {
+			return p.MetaRefinerySendReason != ""
+		},
 		appendMsgp: func(p *Payload, in []byte) (out []byte, ok bool) {
 			if p.MetaRefinerySendReason != "" {
 				out = msgp.AppendString(in, MetaRefinerySendReason)
@@ -513,6 +555,9 @@ var metadataFields = []metadataField{
 				p.MetaSpanEventCount = v
 			}
 		},
+		exist: func(p *Payload) bool {
+			return p.MetaSpanEventCount != 0
+		},
 		appendMsgp: func(p *Payload, in []byte) (out []byte, ok bool) {
 			if p.MetaSpanEventCount != 0 {
 				out = msgp.AppendString(in, MetaSpanEventCount)
@@ -540,6 +585,9 @@ var metadataFields = []metadataField{
 				p.MetaSpanLinkCount = v
 			}
 		},
+		exist: func(p *Payload) bool {
+			return p.MetaSpanLinkCount != 0
+		},
 		appendMsgp: func(p *Payload, in []byte) (out []byte, ok bool) {
 			if p.MetaSpanLinkCount != 0 {
 				out = msgp.AppendString(in, MetaSpanLinkCount)
@@ -567,6 +615,9 @@ var metadataFields = []metadataField{
 				p.MetaSpanCount = v
 			}
 		},
+		exist: func(p *Payload) bool {
+			return p.MetaSpanCount != 0
+		},
 		appendMsgp: func(p *Payload, in []byte) (out []byte, ok bool) {
 			if p.MetaSpanCount != 0 {
 				out = msgp.AppendString(in, MetaSpanCount)
@@ -594,6 +645,9 @@ var metadataFields = []metadataField{
 				p.MetaEventCount = v
 			}
 		},
+		exist: func(p *Payload) bool {
+			return p.MetaEventCount != 0
+		},
 		appendMsgp: func(p *Payload, in []byte) (out []byte, ok bool) {
 			if p.MetaEventCount != 0 {
 				out = msgp.AppendString(in, MetaEventCount)
@@ -621,6 +675,9 @@ var metadataFields = []metadataField{
 				p.MetaRefineryOriginalSampleRate = v
 			}
 		},
+		exist: func(p *Payload) bool {
+			return p.MetaRefineryOriginalSampleRate != 0
+		},
 		appendMsgp: func(p *Payload, in []byte) (out []byte, ok bool) {
 			if p.MetaRefineryOriginalSampleRate != 0 {
 				out = msgp.AppendString(in, MetaRefineryOriginalSampleRate)
@@ -648,6 +705,9 @@ var metadataFields = []metadataField{
 				p.MetaRefineryShutdownSend.Set(v)
 			}
 		},
+		exist: func(p *Payload) bool {
+			return p.MetaRefineryShutdownSend.HasValue
+		},
 		appendMsgp: func(p *Payload, in []byte) (out []byte, ok bool) {
 			if p.MetaRefineryShutdownSend.HasValue {
 				out = msgp.AppendString(in, MetaRefineryShutdownSend)
@@ -679,6 +739,9 @@ var metadataFields = []metadataField{
 				p.MetaRefinerySampleKey = v
 			}
 		},
+		exist: func(p *Payload) bool {
+			return p.MetaRefinerySampleKey != ""
+		},
 		appendMsgp: func(p *Payload, in []byte) (out []byte, ok bool) {
 			if p.MetaRefinerySampleKey != "" {
 				out = msgp.AppendString(in, MetaRefinerySampleKey)
@@ -736,6 +799,8 @@ type Payload struct {
 
 	hasExtractedMetadata bool
 
+	config config.Config
+
 	// Cached metadata fields for efficient access
 	MetaSignalType                string       // meta.signal_type
 	MetaTraceID                   string       // meta.trace_id
@@ -764,11 +829,14 @@ type Payload struct {
 
 // extractMetadataFromBytes extracts metadata from msgpack data.
 // If consumed is non-nil, it will be set to the number of bytes consumed from the data.
-func (p *Payload) extractMetadataFromBytes(data []byte, traceIdFieldNames, parentIdFieldNames []string) (int, error) {
+func (p *Payload) extractMetadataFromBytes(data []byte) (int, error) {
 	if !p.MetaRefineryRoot.HasValue {
 		p.MetaRefineryRoot.Set(true)
 	}
 
+	traceIdFieldNames := p.config.GetTraceIdFieldNames()
+	parentIdFieldNames := p.config.GetParentIdFieldNames()
+
 	// Read the map header
 	mapSize, remaining, err := msgp.ReadMapHeaderBytes(data)
 	if err != nil {
@@ -859,10 +927,9 @@ func (p *Payload) extractMetadataFromBytes(data []byte, traceIdFieldNames, paren
 }
 
 // ExtractMetadata populates the cached metadata fields from the payload data.
-// This MUST be called manually after creating or unmarshaling a non-empty Payload
-// to populate the metadata fields. The traceIdFieldNames and parentIdFieldNames parameters
-// are optional and used to extract trace ID and determine if the span is a root span.
-func (p *Payload) ExtractMetadata(traceIdFieldNames, parentIdFieldNames []string) error {
+// This MUST be called manually after creating a non-empty Payload
+// to populate the metadata fields.
+func (p *Payload) ExtractMetadata() error {
 	if p.hasExtractedMetadata {
 		return nil
 	}
@@ -871,6 +938,12 @@ func (p *Payload) ExtractMetadata(traceIdFieldNames, parentIdFieldNames []string
 		p.MetaRefineryRoot.Set(true)
 	}
 
+	var traceIdFieldNames, parentIdFieldNames []string
+	if p.config != nil {
+		traceIdFieldNames = p.config.GetTraceIdFieldNames()
+		parentIdFieldNames = p.config.GetParentIdFieldNames()
+	}
+
 	// For memoized fields, directly access the map
 	if p.memoizedFields != nil {
 		for key, value := range p.memoizedFields {
@@ -915,7 +988,7 @@ func (p *Payload) ExtractMetadata(traceIdFieldNames, parentIdFieldNames []string
 
 	// For msgpMap fields, extract from the raw bytes
 	if p.msgpMap.Size() > 0 {
-		_, err := p.extractMetadataFromBytes(p.msgpMap.rawData, traceIdFieldNames, parentIdFieldNames)
+		_, err := p.extractMetadataFromBytes(p.msgpMap.rawData)
 		if err != nil {
 			return err
 		}
@@ -931,11 +1004,13 @@ func (p *Payload) ExtractMetadata(traceIdFieldNames, parentIdFieldNames []string
 }
 
 // NewPayload creates a new Payload from a map of fields. This is not populate
-// metadata fields; to do this, you MUST call ExtractMetadata.
-func NewPayload(data map[string]any) Payload {
+// metadata fields
+func NewPayload(config config.Config, data map[string]any) Payload {
 	p := Payload{
 		memoizedFields: data,
 	}
+
+	p.config = config
 	return p
 }
 
@@ -943,29 +1018,15 @@ func NewPayload(data map[string]any) Payload {
 // keep a reference to serialized data.
 func (p *Payload) UnmarshalMsgpack(data []byte) error {
 	p.msgpMap = MsgpPayloadMap{rawData: data}
+	p.ExtractMetadata()
 	return nil
 }
 
 // UnmarshalMsg implements msgp.Unmarshaler, similar to above but expects to be
 // part of a larger message.
 func (p *Payload) UnmarshalMsg(bts []byte) (o []byte, err error) {
-	// Oddly the msgp library doesn't export the internal size method it uses
-	// to skip data. So we will derived it from the returned slice.
-	remainder, err := msgp.Skip(bts)
-	if err != nil {
-		return nil, err
-	}
-	ourData := bts[:len(bts)-len(remainder)]
-	p.msgpMap = MsgpPayloadMap{rawData: ourData}
-	return remainder, err
-}
-
-// UnmarshalMsgWithMetadata unmarshals the payload and extracts metadata in a single pass.
-// This is more efficient than calling UnmarshalMsg followed by ExtractMetadata separately.
-// It returns the remaining bytes after unmarshaling.
-func (p *Payload) UnmarshalMsgWithMetadata(bts []byte, traceIdFieldNames, parentIdFieldNames []string) (o []byte, err error) {
 	// Extract metadata and get consumed bytes
-	consumed, err := p.extractMetadataFromBytes(bts, traceIdFieldNames, parentIdFieldNames)
+	consumed, err := p.extractMetadataFromBytes(bts)
 	if err != nil {
 		return nil, err
 	}
@@ -984,6 +1045,7 @@ func (p *Payload) UnmarshalJSON(data []byte) error {
 		return err
 	}
 	p.memoizedFields = fields
+	p.ExtractMetadata()
 	return nil
 }
 
@@ -1043,6 +1105,13 @@ func (p *Payload) MemoizeFields(keys ...string) {
 }
 
 func (p *Payload) Exists(key string) bool {
+	// if the key is a metadata field, check the dedicated field
+	for _, field := range metadataFields {
+		if field.key == key {
+			return field.exist(p)
+		}
+	}
+
 	if p.memoizedFields != nil {
 		if _, ok := p.memoizedFields[key]; ok {
 			return true
@@ -1152,6 +1221,13 @@ func (p *Payload) All() iter.Seq2[string, any] {
 	return func(yield func(string, any) bool) {
 		// First yield metadata fields with non-default values
 		for _, field := range metadataFields {
+			if field.key == MetaTraceID {
+				continue
+			}
+			if field.key == MetaRefineryRoot && !p.MetaRefineryRoot.Value {
+				// Skip the root field if it's false, as it doesn't need to be yielded
+				continue
+			}
 			if value, ok := field.get(p); ok {
 				if !yield(field.key, value) {
 					return
@@ -1247,7 +1323,12 @@ func getByteSize(val any) int {
 // containing all fields from both memoizedFields and msgpMap. This is incredibly
 // inefficient and is only here (for now) to support our legacy nested field implementation.
 func (p Payload) MarshalJSON() ([]byte, error) {
-	return json.Marshal(maps.Collect(p.All()))
+	data := maps.Collect(p.All())
+	maps.DeleteFunc(data, func(k string, v any) bool {
+		return k == MetaTraceID
+	})
+
+	return json.Marshal(data)
 }
 
 // Implements msgpack.Marshaler.


Test patch:
diff --git a/app/app_test.go b/app/app_test.go
index 890b4c2..6aca48f 100644
--- a/app/app_test.go
+++ b/app/app_test.go
@@ -50,10 +50,16 @@ type testAPIServer struct {
 	server *httptest.Server
 	events []*types.Event
 	mutex  sync.RWMutex
+	cfg    *config.MockConfig
 }
 
 func newTestAPIServer(t testing.TB) *testAPIServer {
-	server := &testAPIServer{}
+	server := &testAPIServer{
+		cfg: &config.MockConfig{
+			TraceIdFieldNames:  []string{"trace.trace_id"},
+			ParentIdFieldNames: []string{"trace.parent_id"},
+		},
+	}
 
 	mux := http.NewServeMux()
 	mux.HandleFunc("/1/batch/", server.handleBatch)
@@ -136,8 +142,8 @@ func (t *testAPIServer) handleBatch(w http.ResponseWriter, r *http.Request) {
 			Timestamp:  timestamp,
 		}
 		if dataMap, ok := eventData["data"].(map[string]interface{}); ok {
-			event.Data = types.NewPayload(dataMap)
-			event.Data.ExtractMetadata([]string{"trace.trace_id"}, []string{"trace.parent_id"})
+			event.Data = types.NewPayload(t.cfg, dataMap)
+			event.Data.ExtractMetadata()
 		}
 
 		t.events = append(t.events, event)
diff --git a/collect/collect_test.go b/collect/collect_test.go
index 615860a..d411071 100644
--- a/collect/collect_test.go
+++ b/collect/collect_test.go
@@ -15,7 +15,6 @@ import (
 	"github.com/jonboulle/clockwork"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
-	"github.com/vmihailenco/msgpack/v5"
 	"go.opentelemetry.io/otel/trace/noop"
 
 	"github.com/honeycombio/refinery/collect/cache"
@@ -125,6 +124,7 @@ func TestAddRootSpan(t *testing.T) {
 			SizeCheckInterval: config.Duration(1 * time.Second),
 		},
 		GetSamplerTypeVal:  &config.DeterministicSamplerConfig{SampleRate: 1},
+		TraceIdFieldNames:  []string{"trace.trace_id", "traceId"},
 		ParentIdFieldNames: []string{"trace.parent_id", "parentId"},
 		GetCollectionConfigVal: config.CollectionConfig{
 			ShutdownDelay:     config.Duration(1 * time.Millisecond),
@@ -205,13 +205,13 @@ func TestAddRootSpan(t *testing.T) {
 		Event: types.Event{
 			Dataset: "aoeu",
 			APIKey:  legacyAPIKey,
-			Data: types.NewPayload(map[string]interface{}{
+			Data: types.NewPayload(coll.Config, map[string]interface{}{
 				"meta.refinery.min_span": true,
 			}),
 		},
 		IsRoot: true,
 	}
-	span.Event.Data.ExtractMetadata(nil, nil)
+	span.Event.Data.ExtractMetadata()
 
 	coll.AddSpanFromPeer(span)
 	// adding one root decision span with no parent ID should:
@@ -266,6 +266,7 @@ func TestOriginalSampleRateIsNotedInMetaField(t *testing.T) {
 			SizeCheckInterval: config.Duration(1 * time.Second),
 		},
 		GetSamplerTypeVal:  &config.DeterministicSamplerConfig{SampleRate: expectedDeterministicSampleRate},
+		TraceIdFieldNames:  []string{"trace.trace_id", "traceId"},
 		ParentIdFieldNames: []string{"trace.parent_id", "parentId"},
 		GetCollectionConfigVal: config.CollectionConfig{
 			ShutdownDelay:     config.Duration(1 * time.Millisecond),
@@ -294,7 +295,7 @@ func TestOriginalSampleRateIsNotedInMetaField(t *testing.T) {
 				Dataset:    "aoeu",
 				APIKey:     legacyAPIKey,
 				SampleRate: originalSampleRate,
-				Data:       types.NewPayload(make(map[string]interface{})),
+				Data:       types.NewPayload(coll.Config, make(map[string]interface{})),
 			},
 			IsRoot: true,
 		}
@@ -320,7 +321,7 @@ func TestOriginalSampleRateIsNotedInMetaField(t *testing.T) {
 			Dataset:    "no-upstream-sampling",
 			APIKey:     legacyAPIKey,
 			SampleRate: 0, // no upstream sampling
-			Data:       types.NewPayload(make(map[string]interface{})),
+			Data:       types.NewPayload(coll.Config, make(map[string]interface{})),
 		},
 		IsRoot: true,
 	})
@@ -361,6 +362,7 @@ func TestTransmittedSpansShouldHaveASampleRateOfAtLeastOne(t *testing.T) {
 			SizeCheckInterval: config.Duration(1 * time.Second),
 		},
 		GetSamplerTypeVal:  &config.DeterministicSamplerConfig{SampleRate: 1},
+		TraceIdFieldNames:  []string{"trace.trace_id", "traceId"},
 		ParentIdFieldNames: []string{"trace.parent_id", "parentId"},
 		GetCollectionConfigVal: config.CollectionConfig{
 			ShutdownDelay:     config.Duration(1 * time.Millisecond),
@@ -385,7 +387,7 @@ func TestTransmittedSpansShouldHaveASampleRateOfAtLeastOne(t *testing.T) {
 			Dataset:    "aoeu",
 			APIKey:     legacyAPIKey,
 			SampleRate: 0, // This should get lifted to 1
-			Data:       types.NewPayload(make(map[string]interface{})),
+			Data:       types.NewPayload(coll.Config, nil),
 		},
 		IsRoot: true,
 	}
@@ -413,6 +415,7 @@ func TestAddSpan(t *testing.T) {
 			SizeCheckInterval: config.Duration(1 * time.Second),
 		},
 		GetSamplerTypeVal:  &config.DeterministicSamplerConfig{SampleRate: 1},
+		TraceIdFieldNames:  []string{"trace.trace_id", "traceId"},
 		ParentIdFieldNames: []string{"trace.parent_id", "parentId"},
 		GetCollectionConfigVal: config.CollectionConfig{
 			ShutdownDelay:     config.Duration(1 * time.Millisecond),
@@ -437,13 +440,13 @@ func TestAddSpan(t *testing.T) {
 		TraceID: traceID,
 		Event: types.Event{
 			Dataset: "aoeu",
-			Data: types.NewPayload(map[string]interface{}{
+			Data: types.NewPayload(coll.Config, map[string]interface{}{
 				"trace.parent_id": "unused",
 			}),
 			APIKey: legacyAPIKey,
 		},
 	}
-	span.Event.Data.ExtractMetadata(nil, nil)
+	span.Event.Data.ExtractMetadata()
 	coll.AddSpanFromPeer(span)
 
 	assert.EventuallyWithT(t, func(collect *assert.CollectT) {
@@ -458,7 +461,7 @@ func TestAddSpan(t *testing.T) {
 		TraceID: traceID,
 		Event: types.Event{
 			Dataset: "aoeu",
-			Data: types.NewPayload(map[string]interface{}{
+			Data: types.NewPayload(coll.Config, map[string]interface{}{
 				"trace.parent_id":        "unused",
 				"meta.refinery.min_span": true,
 				"meta.refinery.send_by":  sendBy,
@@ -466,7 +469,7 @@ func TestAddSpan(t *testing.T) {
 			APIKey: legacyAPIKey,
 		},
 	}
-	spanFromPeer.Event.Data.ExtractMetadata(nil, nil)
+	spanFromPeer.Event.Data.ExtractMetadata()
 
 	coll.AddSpanFromPeer(spanFromPeer)
 	time.Sleep(conf.GetTracesConfig().GetSendTickerValue() * 2)
@@ -483,12 +486,12 @@ func TestAddSpan(t *testing.T) {
 		TraceID: traceID,
 		Event: types.Event{
 			Dataset: "aoeu",
-			Data:    types.NewPayload(map[string]interface{}{}),
+			Data:    types.NewPayload(coll.Config, nil),
 			APIKey:  legacyAPIKey,
 		},
 		IsRoot: true,
 	}
-	rootSpan.Event.Data.ExtractMetadata(nil, nil)
+	rootSpan.Event.Data.ExtractMetadata()
 	coll.AddSpan(rootSpan)
 
 	assert.Equal(t, 2, len(transmission.GetBlock(2)), "adding a root span should send all spans in the trace")
@@ -514,6 +517,7 @@ func TestDryRunMode(t *testing.T) {
 			SampleRate: 10,
 		},
 		DryRun:             true,
+		TraceIdFieldNames:  []string{"trace.trace_id", "traceId"},
 		ParentIdFieldNames: []string{"trace.parent_id", "parentId"},
 		GetCollectionConfigVal: config.CollectionConfig{
 			ShutdownDelay:     config.Duration(1 * time.Millisecond),
@@ -559,7 +563,7 @@ func TestDryRunMode(t *testing.T) {
 	span := &types.Span{
 		TraceID: traceID1,
 		Event: types.Event{
-			Data:   types.NewPayload(map[string]interface{}{}),
+			Data:   types.NewPayload(coll.Config, nil),
 			APIKey: legacyAPIKey,
 		},
 		IsRoot: true,
@@ -580,7 +584,7 @@ func TestDryRunMode(t *testing.T) {
 		TraceID: traceID2,
 		Event: types.Event{
 			Dataset: "aoeu",
-			Data: types.NewPayload(map[string]interface{}{
+			Data: types.NewPayload(coll.Config, map[string]interface{}{
 				"trace.parent_id": "unused",
 			}),
 			APIKey: legacyAPIKey,
@@ -595,7 +599,7 @@ func TestDryRunMode(t *testing.T) {
 	span = &types.Span{
 		TraceID: traceID2,
 		Event: types.Event{
-			Data:   types.NewPayload(map[string]interface{}{}),
+			Data:   types.NewPayload(coll.Config, nil),
 			APIKey: legacyAPIKey,
 		},
 		IsRoot: true,
@@ -617,7 +621,7 @@ func TestDryRunMode(t *testing.T) {
 	span = &types.Span{
 		TraceID: traceID3,
 		Event: types.Event{
-			Data:   types.NewPayload(map[string]interface{}{}),
+			Data:   types.NewPayload(coll.Config, nil),
 			APIKey: legacyAPIKey,
 		},
 		IsRoot: true,
@@ -644,6 +648,7 @@ func TestSampleConfigReload(t *testing.T) {
 			MaxBatchSize: 500,
 		},
 		GetSamplerTypeVal:      &config.DeterministicSamplerConfig{SampleRate: 1},
+		TraceIdFieldNames:      []string{"trace.trace_id", "traceId"},
 		ParentIdFieldNames:     []string{"trace.parent_id", "parentId"},
 		GetCollectionConfigVal: config.CollectionConfig{CacheCapacity: 10, ShutdownDelay: config.Duration(1 * time.Millisecond)},
 		SampleCache: config.SampleCacheConfig{
@@ -728,6 +733,7 @@ func TestStableMaxAlloc(t *testing.T) {
 			SizeCheckInterval: config.Duration(1 * time.Second),
 		},
 		GetSamplerTypeVal:  &config.DeterministicSamplerConfig{SampleRate: 1},
+		TraceIdFieldNames:  []string{"trace.trace_id", "traceId"},
 		ParentIdFieldNames: []string{"trace.parent_id", "parentId"},
 		GetCollectionConfigVal: config.CollectionConfig{
 			ShutdownDelay:     config.Duration(1 * time.Millisecond),
@@ -766,7 +772,7 @@ func TestStableMaxAlloc(t *testing.T) {
 			TraceID: strconv.Itoa(i),
 			Event: types.Event{
 				Dataset: "aoeu",
-				Data:    types.NewPayload(spandata[i]),
+				Data:    types.NewPayload(coll.Config, spandata[i]),
 				APIKey:  legacyAPIKey,
 			},
 		}
@@ -849,6 +855,7 @@ func TestAddSpanNoBlock(t *testing.T) {
 			MaxBatchSize: 500,
 		},
 		GetSamplerTypeVal:  &config.DeterministicSamplerConfig{},
+		TraceIdFieldNames:  []string{"trace.trace_id", "traceId"},
 		ParentIdFieldNames: []string{"trace.parent_id", "parentId"},
 		GetCollectionConfigVal: config.CollectionConfig{
 			ShutdownDelay: config.Duration(1 * time.Millisecond),
@@ -942,6 +949,7 @@ func TestAddCountsToRoot(t *testing.T) {
 		GetSamplerTypeVal:  &config.DeterministicSamplerConfig{SampleRate: 1},
 		AddSpanCountToRoot: true,
 		AddCountsToRoot:    true,
+		TraceIdFieldNames:  []string{"trace.trace_id", "traceId"},
 		ParentIdFieldNames: []string{"trace.parent_id", "parentId"},
 		GetCollectionConfigVal: config.CollectionConfig{
 			ShutdownDelay: config.Duration(1 * time.Millisecond),
@@ -966,7 +974,7 @@ func TestAddCountsToRoot(t *testing.T) {
 			TraceID: traceID,
 			Event: types.Event{
 				Dataset: "aoeu",
-				Data: types.NewPayload(map[string]interface{}{
+				Data: types.NewPayload(coll.Config, map[string]interface{}{
 					"trace.parent_id": "unused",
 				}),
 				APIKey: legacyAPIKey,
@@ -990,7 +998,7 @@ func TestAddCountsToRoot(t *testing.T) {
 		TraceID: traceID,
 		Event: types.Event{
 			Dataset: "aoeu",
-			Data:    types.NewPayload(map[string]interface{}{}),
+			Data:    types.NewPayload(coll.Config, nil),
 			APIKey:  legacyAPIKey,
 		},
 		IsRoot: true,
@@ -1051,6 +1059,7 @@ func TestLateRootGetsCounts(t *testing.T) {
 		GetSamplerTypeVal:    &config.DeterministicSamplerConfig{SampleRate: 1},
 		AddSpanCountToRoot:   true,
 		AddCountsToRoot:      true,
+		TraceIdFieldNames:    []string{"trace.trace_id", "traceId"},
 		ParentIdFieldNames:   []string{"trace.parent_id", "parentId"},
 		AddRuleReasonToTrace: true,
 		GetCollectionConfigVal: config.CollectionConfig{
@@ -1078,7 +1087,7 @@ func TestLateRootGetsCounts(t *testing.T) {
 			TraceID: traceID,
 			Event: types.Event{
 				Dataset: "aoeu",
-				Data: types.NewPayload(map[string]interface{}{
+				Data: types.NewPayload(coll.Config, map[string]interface{}{
 					"trace.parent_id": "unused",
 				}),
 				APIKey: legacyAPIKey,
@@ -1110,7 +1119,7 @@ func TestLateRootGetsCounts(t *testing.T) {
 		TraceID: traceID,
 		Event: types.Event{
 			Dataset: "aoeu",
-			Data:    types.NewPayload(map[string]interface{}{}),
+			Data:    types.NewPayload(coll.Config, nil),
 			APIKey:  legacyAPIKey,
 		},
 		IsRoot: true,
@@ -1144,6 +1153,7 @@ func TestAddSpanCount(t *testing.T) {
 		},
 		GetSamplerTypeVal:  &config.DeterministicSamplerConfig{SampleRate: 1},
 		AddSpanCountToRoot: true,
+		TraceIdFieldNames:  []string{"trace.trace_id", "traceId"},
 		ParentIdFieldNames: []string{"trace.parent_id", "parentId"},
 		GetCollectionConfigVal: config.CollectionConfig{
 			ShutdownDelay:     config.Duration(1 * time.Millisecond),
@@ -1168,25 +1178,25 @@ func TestAddSpanCount(t *testing.T) {
 		TraceID: traceID,
 		Event: types.Event{
 			Dataset: "aoeu",
-			Data: types.NewPayload(map[string]interface{}{
+			Data: types.NewPayload(coll.Config, map[string]interface{}{
 				"trace.parent_id": "unused",
 			}),
 			APIKey: legacyAPIKey,
 		},
 	}
-	span.Data.ExtractMetadata(conf.GetTraceIdFieldNames(), conf.GetParentIdFieldNames())
+	span.Data.ExtractMetadata()
 	decisionSpan := &types.Span{
 		TraceID: traceID,
 		Event: types.Event{
 			Dataset: "aoeu",
-			Data: types.NewPayload(map[string]interface{}{
+			Data: types.NewPayload(coll.Config, map[string]interface{}{
 				"trace.parent_id":        "unused",
 				"meta.refinery.min_span": true,
 			}),
 			APIKey: legacyAPIKey,
 		},
 	}
-	decisionSpan.Data.ExtractMetadata(conf.GetTraceIdFieldNames(), conf.GetParentIdFieldNames())
+	decisionSpan.Data.ExtractMetadata()
 
 	coll.AddSpanFromPeer(span)
 	coll.AddSpanFromPeer(decisionSpan)
@@ -1201,12 +1211,12 @@ func TestAddSpanCount(t *testing.T) {
 		TraceID: traceID,
 		Event: types.Event{
 			Dataset: "aoeu",
-			Data:    types.NewPayload(map[string]interface{}{}),
+			Data:    types.NewPayload(coll.Config, nil),
 			APIKey:  legacyAPIKey,
 		},
 		IsRoot: true,
 	}
-	rootSpan.Data.ExtractMetadata(nil, nil)
+	rootSpan.Data.ExtractMetadata()
 	coll.AddSpan(rootSpan)
 
 	events := transmission.GetBlock(2)
@@ -1234,6 +1244,7 @@ func TestLateRootGetsSpanCount(t *testing.T) {
 		GetSamplerTypeVal:    &config.DeterministicSamplerConfig{SampleRate: 1},
 		AddSpanCountToRoot:   true,
 		ParentIdFieldNames:   []string{"trace.parent_id", "parentId"},
+		TraceIdFieldNames:    []string{"trace.trace_id", "traceId"},
 		AddRuleReasonToTrace: true,
 		GetCollectionConfigVal: config.CollectionConfig{
 			ShutdownDelay:     config.Duration(1 * time.Millisecond),
@@ -1258,7 +1269,7 @@ func TestLateRootGetsSpanCount(t *testing.T) {
 		TraceID: traceID,
 		Event: types.Event{
 			Dataset: "aoeu",
-			Data: types.NewPayload(map[string]interface{}{
+			Data: types.NewPayload(coll.Config, map[string]interface{}{
 				"trace.parent_id": "unused",
 			}),
 			APIKey: legacyAPIKey,
@@ -1275,7 +1286,7 @@ func TestLateRootGetsSpanCount(t *testing.T) {
 		TraceID: traceID,
 		Event: types.Event{
 			Dataset: "aoeu",
-			Data:    types.NewPayload(map[string]interface{}{}),
+			Data:    types.NewPayload(coll.Config, nil),
 			APIKey:  legacyAPIKey,
 		},
 		IsRoot: true,
@@ -1307,6 +1318,7 @@ func TestLateSpanNotDecorated(t *testing.T) {
 		},
 		GetSamplerTypeVal:  &config.DeterministicSamplerConfig{SampleRate: 1},
 		ParentIdFieldNames: []string{"trace.parent_id", "parentId"},
+		TraceIdFieldNames:  []string{"trace.trace_id", "traceId"},
 		GetCollectionConfigVal: config.CollectionConfig{
 			ShutdownDelay:     config.Duration(1 * time.Millisecond),
 			IncomingQueueSize: 5,
@@ -1331,7 +1343,7 @@ func TestLateSpanNotDecorated(t *testing.T) {
 		TraceID: traceID,
 		Event: types.Event{
 			Dataset: "aoeu",
-			Data: types.NewPayload(map[string]interface{}{
+			Data: types.NewPayload(coll.Config, map[string]interface{}{
 				"trace.parent_id": "unused",
 			}),
 			APIKey: legacyAPIKey,
@@ -1343,7 +1355,7 @@ func TestLateSpanNotDecorated(t *testing.T) {
 		TraceID: traceID,
 		Event: types.Event{
 			Dataset: "aoeu",
-			Data:    types.NewPayload(map[string]interface{}{}),
+			Data:    types.NewPayload(coll.Config, nil),
 			APIKey:  legacyAPIKey,
 		},
 		IsRoot: true,
@@ -1370,7 +1382,9 @@ func TestAddAdditionalAttributes(t *testing.T) {
 			DroppedSize:       100,
 			SizeCheckInterval: config.Duration(1 * time.Second),
 		},
-		GetSamplerTypeVal: &config.DeterministicSamplerConfig{SampleRate: 1},
+		GetSamplerTypeVal:  &config.DeterministicSamplerConfig{SampleRate: 1},
+		TraceIdFieldNames:  []string{"trace.trace_id", "traceId"},
+		ParentIdFieldNames: []string{"trace.parent_id", "parentId"},
 		AdditionalAttributes: map[string]string{
 			"name":  "foo",
 			"other": "bar",
@@ -1399,7 +1413,7 @@ func TestAddAdditionalAttributes(t *testing.T) {
 		TraceID: traceID,
 		Event: types.Event{
 			Dataset: "aoeu",
-			Data: types.NewPayload(map[string]interface{}{
+			Data: types.NewPayload(coll.Config, map[string]interface{}{
 				"trace.parent_id": "unused",
 			}),
 			APIKey: legacyAPIKey,
@@ -1412,7 +1426,7 @@ func TestAddAdditionalAttributes(t *testing.T) {
 		TraceID: traceID,
 		Event: types.Event{
 			Dataset: "aoeu",
-			Data:    types.NewPayload(map[string]interface{}{}),
+			Data:    types.NewPayload(coll.Config, nil),
 			APIKey:  legacyAPIKey,
 		},
 		IsRoot: true,
@@ -1434,6 +1448,7 @@ func TestStressReliefSampleRate(t *testing.T) {
 			MaxBatchSize: 500,
 		},
 		GetSamplerTypeVal:  &config.DeterministicSamplerConfig{SampleRate: 1},
+		TraceIdFieldNames:  []string{"trace.trace_id", "traceId"},
 		ParentIdFieldNames: []string{"trace.parent_id", "parentId"},
 		GetCollectionConfigVal: config.CollectionConfig{
 			ShutdownDelay: config.Duration(1 * time.Millisecond),
@@ -1458,7 +1473,7 @@ func TestStressReliefSampleRate(t *testing.T) {
 		TraceID: traceID,
 		Event: types.Event{
 			Dataset: "aoeu",
-			Data: types.NewPayload(map[string]interface{}{
+			Data: types.NewPayload(coll.Config, map[string]interface{}{
 				"trace.parent_id": "unused",
 			}),
 			APIKey: legacyAPIKey,
@@ -1487,7 +1502,7 @@ func TestStressReliefSampleRate(t *testing.T) {
 		TraceID: traceID,
 		Event: types.Event{
 			Dataset:    "aoeu",
-			Data:       types.NewPayload(map[string]interface{}{}),
+			Data:       types.NewPayload(coll.Config, nil),
 			APIKey:     legacyAPIKey,
 			SampleRate: 10,
 		},
@@ -1523,6 +1538,7 @@ func TestStressReliefDecorateHostname(t *testing.T) {
 			SizeCheckInterval: config.Duration(1 * time.Second),
 		},
 		GetSamplerTypeVal:  &config.DeterministicSamplerConfig{SampleRate: 1},
+		TraceIdFieldNames:  []string{"trace.trace_id", "traceId"},
 		ParentIdFieldNames: []string{"trace.parent_id", "parentId"},
 		StressRelief: config.StressReliefConfig{
 			Mode:              "monitor",
@@ -1557,7 +1573,7 @@ func TestStressReliefDecorateHostname(t *testing.T) {
 		TraceID: traceID,
 		Event: types.Event{
 			Dataset: "aoeu",
-			Data: types.NewPayload(map[string]interface{}{
+			Data: types.NewPayload(coll.Config, map[string]interface{}{
 				"trace.parent_id": "unused",
 			}),
 			APIKey: legacyAPIKey,
@@ -1569,7 +1585,7 @@ func TestStressReliefDecorateHostname(t *testing.T) {
 		TraceID: traceID,
 		Event: types.Event{
 			Dataset: "aoeu",
-			Data:    types.NewPayload(map[string]interface{}{}),
+			Data:    types.NewPayload(coll.Config, nil),
 			APIKey:  legacyAPIKey,
 		},
 		IsRoot: true,
@@ -1633,6 +1649,7 @@ func TestSpanWithRuleReasons(t *testing.T) {
 					},
 				},
 			}},
+		TraceIdFieldNames:    []string{"trace.trace_id", "traceId"},
 		ParentIdFieldNames:   []string{"trace.parent_id", "parentId"},
 		AddRuleReasonToTrace: true,
 		GetCollectionConfigVal: config.CollectionConfig{
@@ -1660,7 +1677,7 @@ func TestSpanWithRuleReasons(t *testing.T) {
 		span := &types.Span{
 			Event: types.Event{
 				Dataset: "aoeu",
-				Data: types.NewPayload(map[string]interface{}{
+				Data: types.NewPayload(coll.Config, map[string]interface{}{
 					"trace.parent_id":  "unused",
 					"http.status_code": 200,
 				}),
@@ -1695,7 +1712,7 @@ func TestSpanWithRuleReasons(t *testing.T) {
 			TraceID: traceID,
 			Event: types.Event{
 				Dataset: "aoeu",
-				Data: types.NewPayload(map[string]interface{}{
+				Data: types.NewPayload(coll.Config, map[string]interface{}{
 					"http.status_code": 200,
 				}),
 				APIKey: legacyAPIKey,
@@ -1733,6 +1750,7 @@ func TestRedistributeTraces(t *testing.T) {
 			SendTicker:   config.Duration(2 * time.Millisecond),
 		},
 		GetSamplerTypeVal:      &config.DeterministicSamplerConfig{SampleRate: 1},
+		TraceIdFieldNames:      []string{"trace.trace_id", "traceId"},
 		ParentIdFieldNames:     []string{"trace.parent_id", "parentId"},
 		GetCollectionConfigVal: config.CollectionConfig{CacheCapacity: 10},
 		SampleCache: config.SampleCacheConfig{
@@ -1775,7 +1793,7 @@ func TestRedistributeTraces(t *testing.T) {
 		Event: types.Event{
 			Dataset: dataset,
 			APIKey:  legacyAPIKey,
-			Data:    types.NewPayload(make(map[string]interface{})),
+			Data:    types.NewPayload(coll.Config, nil),
 		},
 	}
 
@@ -1814,22 +1832,22 @@ func TestRedistributeTraces(t *testing.T) {
 			Dataset: dataset,
 			APIKey:  legacyAPIKey,
 			APIHost: "api1",
-			Data:    types.NewPayload(make(map[string]interface{})),
+			Data:    types.NewPayload(coll.Config, nil),
 		},
 		IsRoot: true,
 	}
-	span.Data.ExtractMetadata(conf.GetTraceIdFieldNames(), conf.GetParentIdFieldNames())
+	span.Data.ExtractMetadata()
 	decisionSpan := &types.Span{
 		TraceID: myTraceID,
 		Event: types.Event{
 			Dataset: dataset,
 			APIKey:  legacyAPIKey,
-			Data: types.NewPayload(map[string]interface{}{
+			Data: types.NewPayload(coll.Config, map[string]interface{}{
 				"meta.refinery.min_span": true,
 			}),
 		},
 	}
-	decisionSpan.Data.ExtractMetadata(conf.GetTraceIdFieldNames(), conf.GetParentIdFieldNames())
+	decisionSpan.Data.ExtractMetadata()
 
 	myTrace := &types.Trace{
 		TraceID:          myTraceID,
@@ -1932,6 +1950,7 @@ func TestDrainTracesOnShutdown(t *testing.T) {
 			MaxBatchSize: 500,
 		},
 		GetSamplerTypeVal:  &config.DeterministicSamplerConfig{SampleRate: 1},
+		TraceIdFieldNames:  []string{"trace.trace_id", "traceId"},
 		ParentIdFieldNames: []string{"trace.parent_id", "parentId"},
 		GetCollectionConfigVal: config.CollectionConfig{
 			ShutdownDelay: config.Duration(100 * time.Millisecond),
@@ -1984,7 +2003,7 @@ func TestDrainTracesOnShutdown(t *testing.T) {
 		{
 			name:                 "Trace already has decision, should be sent",
 			traceID:              "traceID1",
-			span:                 &types.Span{TraceID: "traceID1", Event: types.Event{Dataset: "aoeu", Data: types.NewPayload(make(map[string]interface{}))}},
+			span:                 &types.Span{TraceID: "traceID1", Event: types.Event{Dataset: "aoeu", Data: types.NewPayload(coll.Config, nil)}},
 			preRecordTrace:       true,
 			expectedSent:         1,
 			expectedForwarded:    0,
@@ -1993,7 +2012,7 @@ func TestDrainTracesOnShutdown(t *testing.T) {
 		{
 			name:                 "Trace cannot be decided yet, should be forwarded",
 			traceID:              "traceID2",
-			span:                 &types.Span{TraceID: "traceID2", Event: types.Event{Dataset: "test2", Data: types.NewPayload(make(map[string]interface{}))}},
+			span:                 &types.Span{TraceID: "traceID2", Event: types.Event{Dataset: "test2", Data: types.NewPayload(coll.Config, nil)}},
 			preRecordTrace:       false,
 			expectedSent:         0,
 			expectedForwarded:    1,
@@ -2004,7 +2023,7 @@ func TestDrainTracesOnShutdown(t *testing.T) {
 		{
 			name:    "decision spans that already has decision should be ignored and discarded",
 			traceID: "traceID3",
-			span: &types.Span{TraceID: "traceID3", Event: types.Event{Dataset: "test3", Data: types.NewPayload(map[string]interface{}{
+			span: &types.Span{TraceID: "traceID3", Event: types.Event{Dataset: "test3", Data: types.NewPayload(coll.Config, map[string]interface{}{
 				"meta.refinery.min_span": true,
 			})}},
 			preRecordTrace:       true,
@@ -2015,7 +2034,7 @@ func TestDrainTracesOnShutdown(t *testing.T) {
 		{
 			name:    "decision spans that belongs to other peers should be ignored and discarded",
 			traceID: "traceID2",
-			span: &types.Span{TraceID: "traceID2", Event: types.Event{Dataset: "test4", Data: types.NewPayload(map[string]interface{}{
+			span: &types.Span{TraceID: "traceID2", Event: types.Event{Dataset: "test4", Data: types.NewPayload(coll.Config, map[string]interface{}{
 				"meta.refinery.min_span": true,
 			})}},
 			preRecordTrace:       false,
@@ -2028,7 +2047,7 @@ func TestDrainTracesOnShutdown(t *testing.T) {
 	// Run test cases
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			tt.span.Data.ExtractMetadata(conf.GetTraceIdFieldNames(), conf.GetParentIdFieldNames())
+			tt.span.Data.ExtractMetadata()
 
 			// Optionally record the trace decision beforehand
 			if tt.preRecordTrace {
@@ -2093,6 +2112,7 @@ func TestBigTracesGoEarly(t *testing.T) {
 		AddSpanCountToRoot:   true,
 		AddCountsToRoot:      true,
 		ParentIdFieldNames:   []string{"trace.parent_id", "parentId"},
+		TraceIdFieldNames:    []string{"trace.trace_id", "traceId"},
 		AddRuleReasonToTrace: true,
 		GetCollectionConfigVal: config.CollectionConfig{
 			IncomingQueueSize: 500,
@@ -2120,7 +2140,7 @@ func TestBigTracesGoEarly(t *testing.T) {
 			TraceID: traceID,
 			Event: types.Event{
 				Dataset: "aoeu",
-				Data: types.NewPayload(map[string]interface{}{
+				Data: types.NewPayload(coll.Config, map[string]interface{}{
 					"trace.parent_id": "unused",
 					"index":           i,
 				}),
@@ -2139,7 +2159,7 @@ func TestBigTracesGoEarly(t *testing.T) {
 		TraceID: traceID,
 		Event: types.Event{
 			Dataset: "aoeu",
-			Data:    types.NewPayload(map[string]interface{}{}),
+			Data:    types.NewPayload(coll.Config, nil),
 			APIKey:  legacyAPIKey,
 		},
 		IsRoot: true,
@@ -2166,6 +2186,8 @@ func TestCreateDecisionSpan(t *testing.T) {
 			TraceTimeout: config.Duration(5 * time.Millisecond),
 			MaxBatchSize: 500,
 		},
+		TraceIdFieldNames:  []string{"trace.trace_id", "traceId"},
+		ParentIdFieldNames: []string{"trace.parent_id", "parentId"},
 	}
 
 	transmission := &transmit.MockTransmission{}
@@ -2195,7 +2217,7 @@ func TestCreateDecisionSpan(t *testing.T) {
 		TraceID: traceID1,
 		Event: types.Event{
 			Dataset: "aoeu",
-			Data: types.NewPayload(map[string]interface{}{
+			Data: types.NewPayload(coll.Config, map[string]interface{}{
 				"trace.parent_id":        "unused",
 				"http.status_code":       200,
 				"test":                   1,
@@ -2216,8 +2238,9 @@ func TestCreateDecisionSpan(t *testing.T) {
 		Dataset: "aoeu",
 		APIHost: peerShard.Addr,
 		APIKey:  legacyAPIKey,
-		Data: types.NewPayload(map[string]interface{}{
+		Data: types.NewPayload(coll.Config, map[string]interface{}{
 			"http.status_code": 200,
+			"trace.trace_id":   traceID1,
 			"test":             1,
 		}),
 	}
@@ -2226,7 +2249,6 @@ func TestCreateDecisionSpan(t *testing.T) {
 	expected.Data.MetaRefineryMinSpan.Set(true)
 	expected.Data.MetaRefineryRoot.Set(false)
 	expected.Data.MetaRefinerySpanDataSize = 87
-	expected.Data.MetaTraceID = traceID1
 
 	assert.Equal(t, expected.APIHost, ds.APIHost)
 	assert.Equal(t, expected.APIKey, ds.APIKey)
@@ -2261,6 +2283,7 @@ func TestSendDropDecisions(t *testing.T) {
 			MaxBatchSize: 500,
 		},
 		GetSamplerTypeVal:  &config.DeterministicSamplerConfig{SampleRate: 1},
+		TraceIdFieldNames:  []string{"trace.trace_id", "traceId"},
 		ParentIdFieldNames: []string{"trace.parent_id", "parentId"},
 		GetCollectionConfigVal: config.CollectionConfig{
 			ShutdownDelay:     config.Duration(1 * time.Millisecond),
@@ -2355,6 +2378,7 @@ func TestExpiredTracesCleanup(t *testing.T) {
 		GetSamplerTypeVal:    &config.DeterministicSamplerConfig{SampleRate: 1},
 		AddSpanCountToRoot:   true,
 		AddCountsToRoot:      true,
+		TraceIdFieldNames:    []string{"trace.trace_id", "traceId"},
 		ParentIdFieldNames:   []string{"trace.parent_id", "parentId"},
 		AddRuleReasonToTrace: true,
 	}
@@ -2430,6 +2454,7 @@ func TestSpanLimitSendByPreservation(t *testing.T) {
 			MaxBatchSize: 500,
 		},
 		GetSamplerTypeVal:  &config.DeterministicSamplerConfig{SampleRate: 1},
+		TraceIdFieldNames:  []string{"trace.trace_id", "traceId"},
 		ParentIdFieldNames: []string{"trace.parent_id", "parentId"},
 		GetCollectionConfigVal: config.CollectionConfig{
 			ShutdownDelay: config.Duration(1 * time.Millisecond),
@@ -2475,7 +2500,7 @@ func TestSpanLimitSendByPreservation(t *testing.T) {
 			TraceID: traceID,
 			Event: types.Event{
 				Dataset: "test-dataset",
-				Data: types.NewPayload(map[string]interface{}{
+				Data: types.NewPayload(coll.Config, map[string]interface{}{
 					"trace.parent_id": "unused",
 				}),
 				APIKey: legacyAPIKey,
@@ -2606,7 +2631,7 @@ func BenchmarkCollectorWithSamplers(b *testing.B) {
 							Event: types.Event{
 								Dataset: "benchmark-dataset",
 								APIKey:  "test-api-key",
-								Data:    createBenchmarkSpansWithLargeAttributes(traceID, spanIdx),
+								Data:    createBenchmarkSpansWithLargeAttributes(traceID, spanIdx, collector.Config),
 							},
 						}
 
@@ -2632,7 +2657,7 @@ func BenchmarkCollectorWithSamplers(b *testing.B) {
 	}
 }
 
-func createBenchmarkSpansWithLargeAttributes(traceID string, spanIdx int) types.Payload {
+func createBenchmarkSpansWithLargeAttributes(traceID string, spanIdx int, cfg config.Config) types.Payload {
 	// Large text values for testing
 	largeText := strings.Repeat("x", 1000)
 	mediumText := strings.Repeat("y", 500)
@@ -2695,15 +2720,8 @@ func createBenchmarkSpansWithLargeAttributes(traceID string, spanIdx int) types.
 			"key3": spanIdx + 100,
 		},
 	}
-	dataMsgpack, err := msgpack.Marshal(data)
-	if err != nil {
-		panic(err)
-	}
-	payload := types.Payload{}
-	_, err = payload.UnmarshalMsgWithMetadata(dataMsgpack, []string{"trace.trace_id"}, []string{"trace.parent_id"})
-	if err != nil {
-		panic(err)
-	}
+	payload := types.NewPayload(cfg, data)
+	payload.ExtractMetadata()
 	return payload
 }
 
@@ -2722,6 +2740,7 @@ func setupBenchmarkCollector(b *testing.B, samplerConfig interface{}, sender *mo
 			SizeCheckInterval: config.Duration(1 * time.Second),
 		},
 		GetSamplerTypeVal:  samplerConfig,
+		TraceIdFieldNames:  []string{"trace.trace_id", "traceId"},
 		ParentIdFieldNames: []string{"trace.parent_id", "parentId"},
 		GetCollectionConfigVal: config.CollectionConfig{
 			ShutdownDelay:      config.Duration(1 * time.Millisecond),
diff --git a/route/batched_event_test.go b/route/batched_event_test.go
index 9a1532e..49c43bb 100644
--- a/route/batched_event_test.go
+++ b/route/batched_event_test.go
@@ -5,6 +5,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/honeycombio/refinery/config"
 	"github.com/honeycombio/refinery/types"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -14,62 +15,85 @@ import (
 
 func TestBatchedEventRoundTrip(t *testing.T) {
 	timestamp := time.Now()
+	mockCfg := &config.MockConfig{
+		ParentIdFieldNames: []string{"trace.parent_id"},
+	}
 
-	events := batchedEvents{
+	// Create events slice
+	events := []batchedEvent{
 		{
 			MsgPackTimestamp: &timestamp,
-			Data: types.NewPayload(map[string]any{
-				"string_field": "test_value",
-				"int_field":    int64(42),
-				"float_field":  3.14159,
-				"bool_field":   true,
-				"nil_field":    nil,
+			cfg:              mockCfg,
+			Data: types.NewPayload(mockCfg, map[string]any{
+				"string_field":    "test_value",
+				"trace.parent_id": "parent-123",
+				"int_field":       int64(42),
+				"float_field":     3.14159,
+				"bool_field":      true,
+				"nil_field":       nil,
 			}),
 		},
 		{
 			MsgPackTimestamp: &timestamp,
 			SampleRate:       200,
-			Data: types.NewPayload(map[string]any{
-				"another_string": "hello world",
-				"negative_int":   int64(-123),
-				"large_float":    1234.5678,
-				"false_bool":     false,
-				"nil_value":      nil,
+			cfg:              mockCfg,
+			Data: types.NewPayload(mockCfg, map[string]any{
+				"another_string":  "hello world",
+				"trace.parent_id": "parent-123",
+				"negative_int":    int64(-123),
+				"large_float":     1234.5678,
+				"false_bool":      false,
+				"nil_value":       nil,
 			}),
 		},
 	}
 
-	serialized, err := msgpack.Marshal(events)
+	// Create batchedEvents struct and set events
+	batchStruct := newBatchedEvents(mockCfg)
+	batchStruct.events = events
+
+	serialized, err := msgpack.Marshal(batchStruct.events) // Marshal the slice directly
 	require.NoError(t, err)
 
-	var deserialized batchedEvents
+	// Create new batchedEvents for unmarshaling
+	deserialized := newBatchedEvents(mockCfg)
 	_, err = deserialized.UnmarshalMsg(serialized)
 	require.NoError(t, err)
 
-	require.Len(t, deserialized, 2)
-	for i := range deserialized {
-		assert.WithinDuration(t, timestamp, *deserialized[i].MsgPackTimestamp, 0)
-		assert.Equal(t, events[i].SampleRate, deserialized[i].SampleRate)
-		assert.Equal(t, maps.Collect(events[i].Data.All()), maps.Collect(deserialized[i].Data.All()))
+	require.Len(t, deserialized.events, 2)
+	deserializedEvents := deserialized.events
+	for i := range deserializedEvents {
+		assert.WithinDuration(t, timestamp, *deserializedEvents[i].MsgPackTimestamp, 0)
+		assert.Equal(t, events[i].SampleRate, deserializedEvents[i].SampleRate)
+		assert.Equal(t, maps.Collect(events[i].Data.All()), maps.Collect(deserializedEvents[i].Data.All()))
 	}
 
 	// Test overwriting with another unmarshal, with swapped events order.
 	events[0], events[1] = events[1], events[0]
-	serialized, err = msgpack.Marshal(events)
+	batchStruct.events = events
+	serialized, err = msgpack.Marshal(batchStruct.events)
 	require.NoError(t, err)
 
-	deserialized = deserialized[:0]
+	// Reset the deserialized batch
+	deserialized = newBatchedEvents(mockCfg)
 	_, err = deserialized.UnmarshalMsg(serialized)
 	require.NoError(t, err)
-	require.Len(t, deserialized, 2)
-	for i := range deserialized {
-		assert.WithinDuration(t, timestamp, *deserialized[i].MsgPackTimestamp, 0)
-		assert.Equal(t, events[i].SampleRate, deserialized[i].SampleRate)
-		assert.Equal(t, maps.Collect(events[i].Data.All()), maps.Collect(deserialized[i].Data.All()))
+
+	require.Len(t, deserialized.events, 2)
+	deserializedEvents = deserialized.events
+	for i := range deserializedEvents {
+		assert.WithinDuration(t, timestamp, *deserializedEvents[i].MsgPackTimestamp, 0)
+		assert.Equal(t, events[i].SampleRate, deserializedEvents[i].SampleRate)
+		assert.Equal(t, maps.Collect(events[i].Data.All()), maps.Collect(deserializedEvents[i].Data.All()))
 	}
 }
 
 func TestBatchedEventsUnmarshalMsgWithMetadata(t *testing.T) {
+	mockCfg := &config.MockConfig{
+		TraceIdFieldNames:  []string{"trace.trace_id"},
+		ParentIdFieldNames: []string{"trace.parent_id"},
+	}
+
 	// Create multiple test events
 	events := []struct {
 		traceID    string
@@ -114,30 +138,29 @@ func TestBatchedEventsUnmarshalMsgWithMetadata(t *testing.T) {
 	}
 
 	// Test the optimized unmarshal
-	var batch batchedEvents
-	traceIdFields := []string{"trace.trace_id"}
-	parentIdFields := []string{"trace.parent_id"}
+	batch := newBatchedEvents(mockCfg)
 
-	remaining, err := batch.UnmarshalMsgWithMetadata(buf, traceIdFields, parentIdFields)
+	remaining, err := batch.UnmarshalMsg(buf)
 	require.NoError(t, err)
 	assert.Empty(t, remaining)
 
 	// Verify all events were unmarshaled with metadata
-	require.Len(t, batch, 3)
+	batchEvents := batch.events
+	require.Len(t, batchEvents, 3)
 
 	// Event 0: root span (no parent)
-	assert.Equal(t, "trace-1", batch[0].Data.MetaTraceID)
-	assert.True(t, batch[0].Data.MetaRefineryRoot.HasValue)
-	assert.True(t, batch[0].Data.MetaRefineryRoot.Value)
+	assert.Equal(t, "trace-1", batchEvents[0].Data.MetaTraceID)
+	assert.True(t, batchEvents[0].Data.MetaRefineryRoot.HasValue)
+	assert.True(t, batchEvents[0].Data.MetaRefineryRoot.Value)
 
 	// Event 1: non-root span (has parent)
-	assert.Equal(t, "trace-2", batch[1].Data.MetaTraceID)
-	assert.True(t, batch[1].Data.MetaRefineryRoot.HasValue)
-	assert.False(t, batch[1].Data.MetaRefineryRoot.Value)
+	assert.Equal(t, "trace-2", batchEvents[1].Data.MetaTraceID)
+	assert.True(t, batchEvents[1].Data.MetaRefineryRoot.HasValue)
+	assert.False(t, batchEvents[1].Data.MetaRefineryRoot.Value)
 
 	// Event 2: log (never root)
-	assert.Equal(t, "trace-3", batch[2].Data.MetaTraceID)
-	assert.Equal(t, "log", batch[2].Data.MetaSignalType)
-	assert.False(t, batch[2].Data.MetaRefineryRoot.HasValue)
-	assert.False(t, batch[2].Data.MetaRefineryRoot.Value)
+	assert.Equal(t, "trace-3", batchEvents[2].Data.MetaTraceID)
+	assert.Equal(t, "log", batchEvents[2].Data.MetaSignalType)
+	assert.False(t, batchEvents[2].Data.MetaRefineryRoot.HasValue)
+	assert.False(t, batchEvents[2].Data.MetaRefineryRoot.Value)
 }
diff --git a/route/route_test.go b/route/route_test.go
index 327fecd..d1f0afb 100644
--- a/route/route_test.go
+++ b/route/route_test.go
@@ -121,6 +121,10 @@ func TestDecompression(t *testing.T) {
 
 func TestUnmarshal(t *testing.T) {
 	now := time.Now().UTC()
+	mockCfg := &config.MockConfig{
+		TraceIdFieldNames:  []string{"trace.trace_id"},
+		ParentIdFieldNames: []string{"trace.span_id"},
+	}
 
 	// Common test data - using only floats to avoid JSON type conversion issues
 	testData := map[string]interface{}{
@@ -190,19 +194,22 @@ func TestUnmarshal(t *testing.T) {
 	// Test batchedEvents unmarshaling (used in batch)
 	t.Run("batchedEvents", func(t *testing.T) {
 		t.Run("json", func(t *testing.T) {
-			batchEvents := batchedEvents{
+			batch := newBatchedEvents(mockCfg)
+			batch.events = []batchedEvent{
 				{
 					Timestamp:  now.Format(time.RFC3339Nano),
 					SampleRate: 2,
-					Data:       types.NewPayload(testData),
+					Data:       types.NewPayload(mockCfg, testData),
+					cfg:        mockCfg,
 				},
 				{
 					Timestamp:  now.Add(time.Second).Format(time.RFC3339Nano),
 					SampleRate: 4,
-					Data:       types.NewPayload(testData),
+					Data:       types.NewPayload(mockCfg, testData),
+					cfg:        mockCfg,
 				},
 			}
-			jsonData, err := json.Marshal(batchEvents)
+			jsonData, err := json.Marshal(batch)
 			require.NoError(t, err)
 
 			for _, contentType := range []string{"application/json", "application/json; charset=utf-8"} {
@@ -210,18 +217,18 @@ func TestUnmarshal(t *testing.T) {
 					req := httptest.NewRequest("POST", "/test", bytes.NewReader(jsonData))
 					req.Header.Set("Content-Type", contentType)
 
-					var result batchedEvents
-					err = unmarshal(req, readAll(t, req.Body), &result)
+					result := newBatchedEvents(mockCfg)
+					err = unmarshal(req, readAll(t, req.Body), result)
 					require.NoError(t, err)
-					require.Len(t, result, 2)
+					require.Len(t, result.events, 2)
 
-					assert.Equal(t, now.UTC(), result[0].getEventTime())
-					assert.Equal(t, uint(2), result[0].getSampleRate())
-					assert.Equal(t, testData, maps.Collect(result[0].Data.All()))
+					assert.Equal(t, now.UTC(), result.events[0].getEventTime())
+					assert.Equal(t, uint(2), result.events[0].getSampleRate())
+					assert.Equal(t, testData, maps.Collect(result.events[0].Data.All()))
 
-					assert.Equal(t, now.Add(time.Second).UTC(), result[1].getEventTime())
-					assert.Equal(t, uint(4), result[1].getSampleRate())
-					assert.Equal(t, testData, maps.Collect(result[1].Data.All()))
+					assert.Equal(t, now.Add(time.Second).UTC(), result.events[1].getEventTime())
+					assert.Equal(t, uint(4), result.events[1].getSampleRate())
+					assert.Equal(t, testData, maps.Collect(result.events[1].Data.All()))
 				})
 			}
 		})
@@ -258,18 +265,18 @@ func TestUnmarshal(t *testing.T) {
 					req := httptest.NewRequest("POST", "/test", buf)
 					req.Header.Set("Content-Type", contentType)
 
-					var result batchedEvents
-					err = unmarshal(req, readAll(t, req.Body), &result)
+					result := newBatchedEvents(mockCfg)
+					err = unmarshal(req, readAll(t, req.Body), result)
 					require.NoError(t, err)
-					require.Len(t, result, 2)
+					require.Len(t, result.events, 2)
 
-					assert.Equal(t, now.UTC(), result[0].getEventTime())
-					assert.Equal(t, uint(3), result[0].getSampleRate())
-					assert.Equal(t, testData, maps.Collect(result[0].Data.All()))
+					assert.Equal(t, now.UTC(), result.events[0].getEventTime())
+					assert.Equal(t, uint(3), result.events[0].getSampleRate())
+					assert.Equal(t, testData, maps.Collect(result.events[0].Data.All()))
 
-					assert.Equal(t, now.Add(time.Second).UTC(), result[1].getEventTime())
-					assert.Equal(t, uint(6), result[1].getSampleRate())
-					assert.Equal(t, testData, maps.Collect(result[1].Data.All()))
+					assert.Equal(t, now.Add(time.Second).UTC(), result.events[1].getEventTime())
+					assert.Equal(t, uint(6), result.events[1].getSampleRate())
+					assert.Equal(t, testData, maps.Collect(result.events[1].Data.All()))
 
 				})
 			}
@@ -747,6 +754,9 @@ func TestGetDatasetFromRequest(t *testing.T) {
 	}
 }
 func TestExtractMetadataTraceID(t *testing.T) {
+	mockCfg := &config.MockConfig{
+		TraceIdFieldNames: []string{"trace.trace_id", "traceId"},
+	}
 	testCases := []struct {
 		name     string
 		event    types.Event
@@ -755,7 +765,7 @@ func TestExtractMetadataTraceID(t *testing.T) {
 		{
 			name: "trace id from meta.trace_id",
 			event: types.Event{
-				Data: types.NewPayload(map[string]interface{}{
+				Data: types.NewPayload(mockCfg, map[string]interface{}{
 					"meta.trace_id": "trace123",
 				}),
 			},
@@ -764,7 +774,7 @@ func TestExtractMetadataTraceID(t *testing.T) {
 		{
 			name: "trace id from trace.trace_id field",
 			event: types.Event{
-				Data: types.NewPayload(map[string]interface{}{
+				Data: types.NewPayload(mockCfg, map[string]interface{}{
 					"trace.trace_id": "trace456",
 				}),
 			},
@@ -773,7 +783,7 @@ func TestExtractMetadataTraceID(t *testing.T) {
 		{
 			name: "trace id from traceId field",
 			event: types.Event{
-				Data: types.NewPayload(map[string]interface{}{
+				Data: types.NewPayload(mockCfg, map[string]interface{}{
 					"traceId": "trace789",
 				}),
 			},
@@ -782,14 +792,14 @@ func TestExtractMetadataTraceID(t *testing.T) {
 		{
 			name: "no trace id",
 			event: types.Event{
-				Data: types.NewPayload(map[string]interface{}{}),
+				Data: types.NewPayload(mockCfg, nil),
 			},
 			expected: "",
 		},
 		{
 			name: "prefer meta.trace_id over other fields",
 			event: types.Event{
-				Data: types.NewPayload(map[string]interface{}{
+				Data: types.NewPayload(mockCfg, map[string]interface{}{
 					"meta.trace_id":  "meta-trace",
 					"trace.trace_id": "field-trace",
 				}),
@@ -798,11 +808,9 @@ func TestExtractMetadataTraceID(t *testing.T) {
 		},
 	}
 
-	traceIdFieldNames := []string{"trace.trace_id", "traceId"}
-
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
-			tc.event.Data.ExtractMetadata(traceIdFieldNames, nil)
+			tc.event.Data.ExtractMetadata()
 			assert.Equal(t, tc.expected, tc.event.Data.MetaTraceID)
 		})
 	}
@@ -810,7 +818,7 @@ func TestExtractMetadataTraceID(t *testing.T) {
 
 func TestAddIncomingUserAgent(t *testing.T) {
 	t.Run("no incoming user agent", func(t *testing.T) {
-		payload := types.NewPayload(map[string]interface{}{})
+		payload := types.NewPayload(&config.MockConfig{}, nil)
 		event := &types.Event{
 			Data: payload,
 		}
@@ -820,10 +828,10 @@ func TestAddIncomingUserAgent(t *testing.T) {
 	})
 
 	t.Run("existing incoming user agent", func(t *testing.T) {
-		payload := types.NewPayload(map[string]interface{}{
+		payload := types.NewPayload(&config.MockConfig{}, map[string]interface{}{
 			"meta.refinery.incoming_user_agent": "test-agent",
 		})
-		payload.ExtractMetadata(nil, nil)
+		payload.ExtractMetadata()
 		event := &types.Event{
 			Data: payload,
 		}
@@ -834,7 +842,6 @@ func TestAddIncomingUserAgent(t *testing.T) {
 }
 
 func TestProcessEventMetrics(t *testing.T) {
-
 	tests := []struct {
 		name           string
 		incomingOrPeer string
@@ -889,7 +896,7 @@ func TestProcessEventMetrics(t *testing.T) {
 	}
 
 	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
+		t.Run(tt.name+"/"+tt.signalType, func(t *testing.T) {
 			mockMetrics := &metrics.MockMetrics{}
 			mockMetrics.Start()
 
@@ -931,13 +938,14 @@ func TestProcessEventMetrics(t *testing.T) {
 				APIHost:   "test.honeycomb.io",
 				Dataset:   "test-dataset",
 				Timestamp: time.Now(),
-				Data: types.NewPayload(map[string]interface{}{
+				Data: types.NewPayload(mockConfig, map[string]interface{}{
 					"trace.trace_id":    "trace-123",
 					"meta.signal_type":  tt.signalType,
 					"test_attribute":    "test_value",
 					"another_attribute": 123,
 				}),
 			}
+			event.Data.ExtractMetadata()
 			span := &types.Span{
 				Event:   *event,
 				TraceID: "trace-123",
@@ -995,13 +1003,14 @@ func newBatchRouter(t testing.TB) *Router {
 	}
 }
 
-func createBatchEvents() batchedEvents {
+func createBatchEvents(mockCfg config.Config) *batchedEvents {
 	now := time.Now().UTC()
-	batchEvents := batchedEvents{
+	batch := newBatchedEvents(mockCfg)
+	batch.events = []batchedEvent{
 		{
 			Timestamp:  now.Format(time.RFC3339Nano),
 			SampleRate: 2,
-			Data: types.NewPayload(map[string]interface{}{
+			Data: types.NewPayload(mockCfg, map[string]interface{}{
 				"trace.trace_id":  "trace-1",
 				"trace.span_id":   "span-1",
 				"trace.parent_id": "",
@@ -1015,7 +1024,7 @@ func createBatchEvents() batchedEvents {
 		{
 			Timestamp:  now.Format(time.RFC3339Nano),
 			SampleRate: 2,
-			Data: types.NewPayload(map[string]interface{}{
+			Data: types.NewPayload(mockCfg, map[string]interface{}{
 				"trace.trace_id":  "trace-1",
 				"trace.span_id":   "span-2",
 				"trace.parent_id": "span-1",
@@ -1029,7 +1038,7 @@ func createBatchEvents() batchedEvents {
 		{
 			Timestamp:  now.Format(time.RFC3339Nano),
 			SampleRate: 4,
-			Data: types.NewPayload(map[string]interface{}{
+			Data: types.NewPayload(mockCfg, map[string]interface{}{
 				"trace.trace_id":  "trace-2",
 				"trace.span_id":   "span-3",
 				"trace.parent_id": "",
@@ -1040,15 +1049,15 @@ func createBatchEvents() batchedEvents {
 			}),
 		},
 	}
-	return batchEvents
+	return batch
 }
 
 func TestRouterBatch(t *testing.T) {
 	t.Parallel()
 
 	router := newBatchRouter(t)
-	batchEvents := createBatchEvents()
-	batchMsgpack, err := msgpack.Marshal(batchEvents)
+	batch := createBatchEvents(router.Config)
+	batchMsgpack, err := msgpack.Marshal(batch.events)
 	require.NoError(t, err)
 
 	// Create HTTP request directly without server
@@ -1068,7 +1077,7 @@ func TestRouterBatch(t *testing.T) {
 	var responses []*BatchResponse
 	err = json.Unmarshal(w.Body.Bytes(), &responses)
 	require.NoError(t, err)
-	assert.Len(t, responses, len(batchEvents))
+	assert.Len(t, responses, len(batch.events))
 
 	// Verify all responses are successful
 	for i, resp := range responses {
@@ -1081,7 +1090,7 @@ func TestRouterBatch(t *testing.T) {
 	assert.Equal(t, int64(3), mockMetrics.CounterIncrements["incoming_router_batch_events"])
 
 	var spans []*types.Span
-	for len(spans) < len(batchEvents) {
+	for len(spans) < len(batch.events) {
 		select {
 		case span := <-router.Collector.(*collect.MockCollector).Spans:
 			spans = append(spans, span)
@@ -1091,12 +1100,12 @@ func TestRouterBatch(t *testing.T) {
 		}
 	}
 
-	assert.Len(t, spans, len(batchEvents))
+	assert.Len(t, spans, len(batch.events))
 	for i, span := range spans {
-		assert.Equal(t, batchEvents[i].Data.Get("trace.trace_id"), span.TraceID)
-		assert.Equal(t, uint(batchEvents[i].SampleRate), span.SampleRate)
+		assert.Equal(t, batch.events[i].Data.Get("trace.trace_id"), span.TraceID)
+		assert.Equal(t, uint(batch.events[i].SampleRate), span.SampleRate)
 		// Compare data values
-		for k, v := range batchEvents[i].Data.All() {
+		for k, v := range batch.events[i].Data.All() {
 			assert.Equal(t, v, span.Data.Get(k), "Data field %s should match", k)
 		}
 	}
@@ -1122,7 +1131,7 @@ func (d *discardResponseWriter) WriteHeader(statusCode int) {
 	// Discard status code
 }
 
-func createBatchEventsWithLargeAttributes(numEvents int) batchedEvents {
+func createBatchEventsWithLargeAttributes(numEvents int, cfg config.Config) *batchedEvents {
 	now := time.Now().UTC()
 
 	// Large text values for testing
@@ -1130,7 +1139,8 @@ func createBatchEventsWithLargeAttributes(numEvents int) batchedEvents {
 	mediumText := strings.Repeat("y", 500)
 	smallText := strings.Repeat("z", 100)
 
-	batchEvents := make(batchedEvents, numEvents)
+	batchEvents := newBatchedEvents(cfg)
+	batchEvents.events = make([]batchedEvent, numEvents)
 
 	for i := 0; i < numEvents; i++ {
 		data := make(map[string]interface{})
@@ -1188,10 +1198,11 @@ func createBatchEventsWithLargeAttributes(numEvents int) batchedEvents {
 		}
 
 		ts := now.Add(time.Duration(i*100) * time.Millisecond)
-		batchEvents[i] = batchedEvent{
+		batchEvents.events[i] = batchedEvent{
 			MsgPackTimestamp: &ts,
 			SampleRate:       2,
-			Data:             types.NewPayload(data),
+			Data:             types.NewPayload(cfg, data),
+			cfg:              cfg,
 		}
 	}
 
@@ -1200,7 +1211,7 @@ func createBatchEventsWithLargeAttributes(numEvents int) batchedEvents {
 
 func BenchmarkRouterBatch(b *testing.B) {
 	router := newBatchRouter(b)
-	batchEvents := createBatchEventsWithLargeAttributes(3)
+	batchEvents := createBatchEventsWithLargeAttributes(3, router.Config)
 	batchMsgpack, err := msgpack.Marshal(batchEvents)
 	require.NoError(b, err)
 
diff --git a/sample/dynamic_ema_test.go b/sample/dynamic_ema_test.go
index 43302b6..947a3dc 100644
--- a/sample/dynamic_ema_test.go
+++ b/sample/dynamic_ema_test.go
@@ -26,10 +26,11 @@ func TestDynamicEMAAddSampleRateKeyToTrace(t *testing.T) {
 	}
 
 	trace := &types.Trace{}
+	mockCfg := &config.MockConfig{}
 	for i := 0; i < spanCount; i++ {
 		trace.AddSpan(&types.Span{
 			Event: types.Event{
-				Data: types.NewPayload(map[string]interface{}{
+				Data: types.NewPayload(mockCfg, map[string]interface{}{
 					"http.status_code": 200,
 					"app.team.id":      float64(4),
 					"important_field":  true,
diff --git a/sample/dynamic_test.go b/sample/dynamic_test.go
index aa9c7bf..0e11867 100644
--- a/sample/dynamic_test.go
+++ b/sample/dynamic_test.go
@@ -27,11 +27,12 @@ func TestDynamicAddSampleRateKeyToTrace(t *testing.T) {
 	}
 
 	trace := &types.Trace{}
+	mockCfg := &config.MockConfig{}
 	for i := 0; i < spanCount; i++ {
 		if i == spanCount-1 {
 			trace.RootSpan = &types.Span{
 				Event: types.Event{
-					Data: types.NewPayload(map[string]interface{}{
+					Data: types.NewPayload(mockCfg, map[string]interface{}{
 						"http.status_code": "200",
 						"service_name":     "test",
 					}),
@@ -40,7 +41,7 @@ func TestDynamicAddSampleRateKeyToTrace(t *testing.T) {
 		}
 		trace.AddSpan(&types.Span{
 			Event: types.Event{
-				Data: types.NewPayload(map[string]interface{}{
+				Data: types.NewPayload(mockCfg, map[string]interface{}{
 					"http.status_code": "200",
 					"url":              "/test",
 				}),
diff --git a/sample/ema_throughput_test.go b/sample/ema_throughput_test.go
index 4803de3..534f627 100644
--- a/sample/ema_throughput_test.go
+++ b/sample/ema_throughput_test.go
@@ -25,11 +25,12 @@ func TestEMAThroughputAddSampleRateKeyToTrace(t *testing.T) {
 		Metrics: &metrics,
 	}
 
+	mockCfg := &config.MockConfig{}
 	trace := &types.Trace{}
 	for i := 0; i < spanCount; i++ {
 		trace.AddSpan(&types.Span{
 			Event: types.Event{
-				Data: types.NewPayload(map[string]interface{}{
+				Data: types.NewPayload(mockCfg, map[string]interface{}{
 					"http.status_code": 200,
 					"app.team.id":      float64(4),
 					"important_field":  true,
diff --git a/sample/rules_test.go b/sample/rules_test.go
index cc0bcc9..7eeafc4 100644
--- a/sample/rules_test.go
+++ b/sample/rules_test.go
@@ -25,6 +25,7 @@ type TestRulesData struct {
 }
 
 func TestRules(t *testing.T) {
+	mockCfg := &config.MockConfig{}
 	data := []TestRulesData{
 		{
 			Rules: &config.RulesBasedSamplerConfig{
@@ -45,7 +46,7 @@ func TestRules(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": int64(1),
 						}),
 					},
@@ -74,7 +75,7 @@ func TestRules(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": int64(2),
 						}),
 					},
@@ -103,7 +104,7 @@ func TestRules(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": int64(1),
 						}),
 					},
@@ -132,7 +133,7 @@ func TestRules(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": int64(1),
 						}),
 					},
@@ -165,7 +166,7 @@ func TestRules(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": int64(1),
 						}),
 					},
@@ -205,7 +206,7 @@ func TestRules(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test":     2.2,
 							"test_two": false,
 						}),
@@ -213,7 +214,7 @@ func TestRules(t *testing.T) {
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test_two": true,
 						}),
 					},
@@ -242,7 +243,7 @@ func TestRules(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": float64(3),
 						}),
 					},
@@ -264,7 +265,7 @@ func TestRules(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": int64(1),
 						}),
 					},
@@ -298,14 +299,14 @@ func TestRules(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"first": int64(1),
 						}),
 					},
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"first": int64(1),
 						}),
 					},
@@ -336,7 +337,7 @@ func TestRules(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"first": int64(9),
 						}),
 					},
@@ -364,7 +365,7 @@ func TestRules(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"first": int64(9),
 						}),
 					},
@@ -392,7 +393,7 @@ func TestRules(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"second": int64(9),
 						}),
 					},
@@ -421,7 +422,7 @@ func TestRules(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"first": "honeycomb",
 						}),
 					},
@@ -450,7 +451,7 @@ func TestRules(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"first": "honeycomb",
 						}),
 					},
@@ -479,7 +480,7 @@ func TestRules(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"first": "honeycomb",
 						}),
 					},
@@ -508,7 +509,7 @@ func TestRules(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": int64(2),
 						}),
 					},
@@ -537,7 +538,7 @@ func TestRules(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"trace.trace_id":  "12345",
 							"trace.span_id":   "654321",
 							"trace.parent_id": "54321",
@@ -547,7 +548,7 @@ func TestRules(t *testing.T) {
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"trace.trace_id":  "12345",
 							"trace.span_id":   "754321",
 							"trace.parent_id": "54321",
@@ -579,7 +580,7 @@ func TestRules(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"trace.trace_id":  "12345",
 							"trace.span_id":   "54321",
 							"meta.span_count": int64(2),
@@ -589,7 +590,7 @@ func TestRules(t *testing.T) {
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"trace.trace_id":  "12345",
 							"trace.span_id":   "654321",
 							"trace.parent_id": "54321",
@@ -599,7 +600,7 @@ func TestRules(t *testing.T) {
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"trace.trace_id":  "12345",
 							"trace.span_id":   "754321",
 							"trace.parent_id": "54321",
@@ -632,7 +633,7 @@ func TestRules(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test2": int64(17),
 						}),
 					},
@@ -661,7 +662,7 @@ func TestRules(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test2": int64(16),
 						}),
 					},
@@ -691,7 +692,7 @@ func TestRules(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test":  int64(2),
 							"test2": int64(17),
 						}),
@@ -724,7 +725,7 @@ func TestRules(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"trace.trace_id":  "12345",
 							"trace.span_id":   "54322",
 							"trace.parent_id": "54321",
@@ -734,7 +735,7 @@ func TestRules(t *testing.T) {
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"trace.trace_id":  "12345",
 							"trace.span_id":   "654321",
 							"trace.parent_id": "54322",
@@ -743,7 +744,7 @@ func TestRules(t *testing.T) {
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"trace.trace_id":  "12345",
 							"trace.span_id":   "754321",
 							"trace.parent_id": "54322",
@@ -752,7 +753,7 @@ func TestRules(t *testing.T) {
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"trace.trace_id":  "12345",
 							"trace.span_id":   "754321",
 							"trace.parent_id": "54322",
@@ -784,7 +785,7 @@ func TestRules(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"trace.trace_id":  "12345",
 							"trace.span_id":   "54322",
 							"trace.parent_id": "54321",
@@ -794,7 +795,7 @@ func TestRules(t *testing.T) {
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"trace.trace_id":  "12345",
 							"trace.span_id":   "654321",
 							"trace.parent_id": "54322",
@@ -803,7 +804,7 @@ func TestRules(t *testing.T) {
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"trace.trace_id":  "12345",
 							"trace.span_id":   "754321",
 							"trace.parent_id": "54322",
@@ -812,7 +813,7 @@ func TestRules(t *testing.T) {
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"trace.trace_id":  "12345",
 							"trace.span_id":   "754321",
 							"trace.parent_id": "54322",
@@ -874,7 +875,7 @@ func TestRules(t *testing.T) {
 
 func TestRulesWithNestedFields(t *testing.T) {
 	// TODO test this with serialized msgpack as well, we want to make sure the behavior is equivalent.
-
+	mockCfg := &config.MockConfig{}
 	data := []TestRulesData{
 		{
 			Rules: &config.RulesBasedSamplerConfig{
@@ -896,7 +897,7 @@ func TestRulesWithNestedFields(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": map[string]interface{}{
 								"test1": "a",
 							},
@@ -928,7 +929,7 @@ func TestRulesWithNestedFields(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test.test1": "a",
 						}),
 					},
@@ -957,7 +958,7 @@ func TestRulesWithNestedFields(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": map[string]interface{}{
 								"test2": "b",
 							},
@@ -988,7 +989,7 @@ func TestRulesWithNestedFields(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": map[string]interface{}{
 								"test1": "a",
 							},
@@ -1021,7 +1022,7 @@ func TestRulesWithNestedFields(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": map[string]interface{}{
 								"test2": "a",
 							},
@@ -1079,6 +1080,7 @@ func TestRulesWithNestedFields(t *testing.T) {
 }
 
 func TestRulesWithDynamicSampler(t *testing.T) {
+	mockCfg := &config.MockConfig{}
 	data := []TestRulesData{
 		{
 			Rules: &config.RulesBasedSamplerConfig{
@@ -1104,7 +1106,7 @@ func TestRulesWithDynamicSampler(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"rule_test":        int64(1),
 							"http.status_code": "200",
 						}),
@@ -1112,7 +1114,7 @@ func TestRulesWithDynamicSampler(t *testing.T) {
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"rule_test":        int64(1),
 							"http.status_code": "200",
 						}),
@@ -1171,6 +1173,7 @@ func TestRulesWithDynamicSampler(t *testing.T) {
 }
 
 func TestRulesWithEMADynamicSampler(t *testing.T) {
+	mockCfg := &config.MockConfig{}
 	data := []TestRulesData{
 		{
 			Rules: &config.RulesBasedSamplerConfig{
@@ -1196,7 +1199,7 @@ func TestRulesWithEMADynamicSampler(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"rule_test":        int64(1),
 							"http.status_code": "200",
 						}),
@@ -1204,7 +1207,7 @@ func TestRulesWithEMADynamicSampler(t *testing.T) {
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"rule_test":        int64(1),
 							"http.status_code": "200",
 						}),
@@ -1262,6 +1265,7 @@ func TestRulesWithEMADynamicSampler(t *testing.T) {
 }
 
 func TestRuleMatchesSpanMatchingSpan(t *testing.T) {
+	mockCfg := &config.MockConfig{}
 	testCases := []struct {
 		name           string
 		spans          []*types.Span
@@ -1275,7 +1279,7 @@ func TestRuleMatchesSpanMatchingSpan(t *testing.T) {
 			spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"rule_test":        int64(1),
 							"http.status_code": "200",
 						}),
@@ -1283,7 +1287,7 @@ func TestRuleMatchesSpanMatchingSpan(t *testing.T) {
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"rule_test":        int64(5),
 							"http.status_code": "500",
 						}),
@@ -1298,7 +1302,7 @@ func TestRuleMatchesSpanMatchingSpan(t *testing.T) {
 			spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"rule_test":        int64(1),
 							"http.status_code": "500",
 						}),
@@ -1306,7 +1310,7 @@ func TestRuleMatchesSpanMatchingSpan(t *testing.T) {
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"rule_test":        int64(5),
 							"http.status_code": "200",
 						}),
@@ -1379,6 +1383,7 @@ func TestRuleMatchesSpanMatchingSpan(t *testing.T) {
 }
 
 func TestRulesDatatypes(t *testing.T) {
+	mockCfg := &config.MockConfig{}
 	data := []TestRulesData{
 		{
 			Rules: &config.RulesBasedSamplerConfig{
@@ -1400,7 +1405,7 @@ func TestRulesDatatypes(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": int64(1),
 						}),
 					},
@@ -1429,7 +1434,7 @@ func TestRulesDatatypes(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": float64(1.01),
 						}),
 					},
@@ -1458,7 +1463,7 @@ func TestRulesDatatypes(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": "foo",
 						}),
 					},
@@ -1487,7 +1492,7 @@ func TestRulesDatatypes(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": true,
 						}),
 					},
@@ -1516,7 +1521,7 @@ func TestRulesDatatypes(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": true,
 						}),
 					},
@@ -1545,7 +1550,7 @@ func TestRulesDatatypes(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": true,
 						}),
 					},
@@ -1574,7 +1579,7 @@ func TestRulesDatatypes(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": 10.,
 						}),
 					},
@@ -1603,7 +1608,7 @@ func TestRulesDatatypes(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": 100,
 						}),
 					},
@@ -1632,7 +1637,7 @@ func TestRulesDatatypes(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": "500",
 						}),
 					},
@@ -1661,7 +1666,7 @@ func TestRulesDatatypes(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": "500",
 						}),
 					},
@@ -1690,7 +1695,7 @@ func TestRulesDatatypes(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": 2,
 						}),
 					},
@@ -1719,7 +1724,7 @@ func TestRulesDatatypes(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": 9.3, // "9.3" is greater than "10.3" when compared as a string
 						}),
 					},
@@ -1748,7 +1753,7 @@ func TestRulesDatatypes(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": "4.13",
 						}),
 					},
@@ -1777,7 +1782,7 @@ func TestRulesDatatypes(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": "fourPointOneThree",
 						}),
 					},
@@ -1806,7 +1811,7 @@ func TestRulesDatatypes(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": "rightValue",
 						}),
 					},
@@ -1835,7 +1840,7 @@ func TestRulesDatatypes(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": 777,
 						}),
 					},
@@ -1864,7 +1869,7 @@ func TestRulesDatatypes(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": int64(11),
 						}),
 					},
@@ -1892,7 +1897,7 @@ func TestRulesDatatypes(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": int64(123),
 						}),
 					},
@@ -1920,7 +1925,7 @@ func TestRulesDatatypes(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": "abc",
 						}),
 					},
@@ -1958,6 +1963,7 @@ func TestRulesDatatypes(t *testing.T) {
 }
 
 func TestRegexpRules(t *testing.T) {
+	mockCfg := &config.MockConfig{}
 	testdata := []struct {
 		re      string
 		value   any
@@ -2013,7 +2019,7 @@ func TestRegexpRules(t *testing.T) {
 			spans := []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": d.value,
 						}),
 					},
@@ -2031,6 +2037,7 @@ func TestRegexpRules(t *testing.T) {
 }
 
 func TestRulesWithDeterministicSampler(t *testing.T) {
+	mockCfg := &config.MockConfig{}
 	data := []TestRulesData{
 		{
 			Rules: &config.RulesBasedSamplerConfig{
@@ -2055,7 +2062,7 @@ func TestRulesWithDeterministicSampler(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"rule_test":        int64(1),
 							"http.status_code": "200",
 						}),
@@ -2063,7 +2070,7 @@ func TestRulesWithDeterministicSampler(t *testing.T) {
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"rule_test":        int64(1),
 							"http.status_code": "200",
 						}),
@@ -2116,6 +2123,7 @@ func TestRulesWithDeterministicSampler(t *testing.T) {
 }
 
 func TestRulesRootSpanContext(t *testing.T) {
+	mockCfg := &config.MockConfig{}
 	data := []TestRulesData{
 		{
 			Rules: &config.RulesBasedSamplerConfig{
@@ -2136,21 +2144,21 @@ func TestRulesRootSpanContext(t *testing.T) {
 				{
 					TraceID: "123testABC", // I am root.
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": "foo",
 						}),
 					},
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"http.status_code": "200",
 						}),
 					},
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test1": 1,
 							"test2": 2.2,
 							"test3": "foo",
@@ -2187,21 +2195,21 @@ func TestRulesRootSpanContext(t *testing.T) {
 				{
 					TraceID: "123testABC", // I am root.
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": "foo",
 						}),
 					},
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"http.status_code": "200",
 						}),
 					},
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test1": 1,
 							"test2": 2.2,
 							"test3": "foo",
@@ -2237,7 +2245,7 @@ func TestRulesRootSpanContext(t *testing.T) {
 				{
 					TraceID: "123testABC", // I am root.
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test":         "foo",
 							"anotherField": 3.5,
 						}),
@@ -2245,7 +2253,7 @@ func TestRulesRootSpanContext(t *testing.T) {
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"http.status_code": "200",
 							"anotherField":     1.1,
 						}),
@@ -2253,7 +2261,7 @@ func TestRulesRootSpanContext(t *testing.T) {
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test1": 1,
 							"test2": 2.2,
 							"test3": "foo",
@@ -2293,7 +2301,7 @@ func TestRulesRootSpanContext(t *testing.T) {
 				{
 					TraceID: "123testABC", // I am root.
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test":         "foo",
 							"anotherField": 50,
 						}),
@@ -2301,7 +2309,7 @@ func TestRulesRootSpanContext(t *testing.T) {
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"http.status_code": "200",
 							"anotherField":     10,
 						}),
@@ -2309,7 +2317,7 @@ func TestRulesRootSpanContext(t *testing.T) {
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test1": 1,
 							"test2": 2.2,
 							"test3": "foo",
@@ -2349,7 +2357,7 @@ func TestRulesRootSpanContext(t *testing.T) {
 				{
 					TraceID: "123testABC", // I am root.
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test":         "foo",
 							"anotherField": 50,
 						}),
@@ -2357,7 +2365,7 @@ func TestRulesRootSpanContext(t *testing.T) {
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"http.status_code": "200",
 							"anotherField":     10,
 						}),
@@ -2365,7 +2373,7 @@ func TestRulesRootSpanContext(t *testing.T) {
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test1": 1,
 							"test2": 2.2,
 							"test3": "foo",
@@ -2398,21 +2406,21 @@ func TestRulesRootSpanContext(t *testing.T) {
 				{
 					TraceID: "123testABC", // I am root.
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": "nope", // I am the root span that does not match.
 						}),
 					},
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"http.status_code": "200",
 						}),
 					},
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test":  "foo", // I am the span that almost matches, but I'm not root.
 							"test1": 1,
 							"test2": 2.2,
@@ -2445,21 +2453,21 @@ func TestRulesRootSpanContext(t *testing.T) {
 				{
 					TraceID: "123testABC", // I am root.
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": "nope",
 						}),
 					},
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"http.status_code": "200",
 						}),
 					},
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test":  "nope",
 							"test1": 1,
 							"test2": 2.2,
@@ -2491,21 +2499,21 @@ func TestRulesRootSpanContext(t *testing.T) {
 				{
 					TraceID: "123testABC", // I am root.
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": "nope",
 						}),
 					},
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"http.status_code": "200",
 						}),
 					},
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"http.status_code": "500",
 						}),
 					},
@@ -2535,21 +2543,21 @@ func TestRulesRootSpanContext(t *testing.T) {
 				{
 					TraceID: "123testABC", // I am root.
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"test": "nope",
 						}),
 					},
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"http.status_code": "200",
 						}),
 					},
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"http.status_code": "500",
 						}),
 					},
@@ -2578,14 +2586,14 @@ func TestRulesRootSpanContext(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"http.status_code": "200",
 						}),
 					},
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"http.status_code": "500",
 						}),
 					},
@@ -2614,14 +2622,14 @@ func TestRulesRootSpanContext(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"foo": 100.01,
 						}),
 					},
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"foo": 99.1,
 							"bar": 100.10,
 						}),
@@ -2651,14 +2659,14 @@ func TestRulesRootSpanContext(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"foo": true,
 						}),
 					},
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"foo": true,
 						}),
 					},
@@ -2691,14 +2699,14 @@ func TestRulesRootSpanContext(t *testing.T) {
 				{
 					TraceID: "abc123",
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"name.of.service": "is not service.name!",
 						}),
 					},
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"foo": true,
 						}),
 					},
@@ -2731,14 +2739,14 @@ func TestRulesRootSpanContext(t *testing.T) {
 				{
 					TraceID: "abc123",
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"service.name": "totally present",
 						}),
 					},
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"foo": true,
 						}),
 					},
@@ -2767,14 +2775,14 @@ func TestRulesRootSpanContext(t *testing.T) {
 			Spans: []*types.Span{
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"service.name": "no trace id on this test span, so it's not root",
 						}),
 					},
 				},
 				{
 					Event: types.Event{
-						Data: types.NewPayload(map[string]interface{}{
+						Data: types.NewPayload(mockCfg, map[string]interface{}{
 							"foo": true,
 						}),
 					},
diff --git a/sample/totalthroughput_test.go b/sample/totalthroughput_test.go
index 1d20b73..dc168a8 100644
--- a/sample/totalthroughput_test.go
+++ b/sample/totalthroughput_test.go
@@ -25,11 +25,12 @@ func TestTotalThroughputAddSampleRateKeyToTrace(t *testing.T) {
 		Metrics: &metrics,
 	}
 
+	config := &config.MockConfig{}
 	trace := &types.Trace{}
 	for i := 0; i < spanCount; i++ {
 		trace.AddSpan(&types.Span{
 			Event: types.Event{
-				Data: types.NewPayload(map[string]interface{}{
+				Data: types.NewPayload(config, map[string]interface{}{
 					"http.status_code": "200",
 				}),
 			},
diff --git a/sample/trace_key_test.go b/sample/trace_key_test.go
index 188e5df..1ecddee 100644
--- a/sample/trace_key_test.go
+++ b/sample/trace_key_test.go
@@ -6,6 +6,7 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/honeycombio/refinery/config"
 	"github.com/honeycombio/refinery/types"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -96,8 +97,9 @@ func TestKeyGeneration(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
+			config := &config.MockConfig{}
 			generator := newTraceKey(tt.fields, tt.useTraceLength)
-			trace := createTestTrace(t, tt.spans)
+			trace := createTestTrace(t, tt.spans, config)
 
 			key, n := generator.build(trace)
 
@@ -115,11 +117,13 @@ func TestKeyLimits(t *testing.T) {
 
 	trace := &types.Trace{}
 
+	config := &config.MockConfig{}
+
 	// generate too many spans with different unique values
 	for i := 0; i < 160; i++ {
 		trace.AddSpan(&types.Span{
 			Event: types.Event{
-				Data: types.NewPayload(map[string]interface{}{
+				Data: types.NewPayload(config, map[string]interface{}{
 					"fieldA": fmt.Sprintf("value%d", i),
 					"fieldB": i,
 				}),
@@ -129,7 +133,7 @@ func TestKeyLimits(t *testing.T) {
 
 	trace.RootSpan = &types.Span{
 		Event: types.Event{
-			Data: types.NewPayload(map[string]interface{}{
+			Data: types.NewPayload(config, map[string]interface{}{
 				"service_name": "test",
 			}),
 		},
@@ -413,6 +417,7 @@ func BenchmarkTraceKeyBuild(b *testing.B) {
 			generator := newTraceKey(scenario.fields, scenario.useTraceLength)
 
 			trace := &types.Trace{}
+			config := &config.MockConfig{}
 
 			for i := 0; i < scenario.spanCount; i++ {
 				spanData := make(map[string]interface{})
@@ -444,7 +449,7 @@ func BenchmarkTraceKeyBuild(b *testing.B) {
 
 				span := &types.Span{
 					Event: types.Event{
-						Data: types.NewPayload(spanData),
+						Data: types.NewPayload(config, spanData),
 					},
 				}
 
@@ -480,13 +485,13 @@ type testSpan struct {
 }
 
 // Helper function to create test traces with specified data
-func createTestTrace(t *testing.T, spans []testSpan) *types.Trace {
+func createTestTrace(t *testing.T, spans []testSpan, config *config.MockConfig) *types.Trace {
 	trace := &types.Trace{}
 
 	for _, s := range spans {
 		span := &types.Span{
 			Event: types.Event{
-				Data: types.NewPayload(s.data),
+				Data: types.NewPayload(config, s.data),
 			},
 		}
 		if s.isRoot {
diff --git a/sample/windowed_throughput_test.go b/sample/windowed_throughput_test.go
index 3746de6..6f2d62f 100644
--- a/sample/windowed_throughput_test.go
+++ b/sample/windowed_throughput_test.go
@@ -26,10 +26,11 @@ func TestWindowedThroughputAddSampleRateKeyToTrace(t *testing.T) {
 	}
 
 	trace := &types.Trace{}
+	mockCfg := &config.MockConfig{}
 	for i := 0; i < spanCount; i++ {
 		trace.AddSpan(&types.Span{
 			Event: types.Event{
-				Data: types.NewPayload(map[string]interface{}{
+				Data: types.NewPayload(mockCfg, map[string]interface{}{
 					"http.status_code": 200,
 					"app.team.id":      float64(4),
 					"important_field":  true,
diff --git a/transmit/direct_transmit_test.go b/transmit/direct_transmit_test.go
index 0c56828..3c91671 100644
--- a/transmit/direct_transmit_test.go
+++ b/transmit/direct_transmit_test.go
@@ -158,11 +158,12 @@ func setupDirectTransmissionTestWithBatchSize(t *testing.T, batchSize int, batch
 // sendTestEvents sends n events to the DirectTransmission with the given server URL
 func sendTestEvents(dt *DirectTransmission, serverURL string, count int, apiKey string) {
 	now := time.Now().UTC()
+	mockCfg := &config.MockConfig{}
 	for i := range count {
-		eventData := types.NewPayload(map[string]any{
+		eventData := types.NewPayload(mockCfg, map[string]any{
 			"event_id": i,
 		})
-		eventData.ExtractMetadata(nil, nil)
+		eventData.ExtractMetadata()
 
 		event := &types.Event{
 			Context:     context.Background(),
@@ -403,11 +404,11 @@ func TestDirectTransmissionErrorHandling(t *testing.T) {
 		dt, mockMetrics, mockLogger := setupDirectTransmissionTest(t)
 
 		// Create an event with data over 1M
-		eventData := types.NewPayload(map[string]any{
+		eventData := types.NewPayload(&config.MockConfig{}, map[string]any{
 			"large_field": strings.Repeat("a", 1024*1024+1000),
 			"event_id":    1,
 		})
-		eventData.ExtractMetadata(nil, nil)
+		eventData.ExtractMetadata()
 
 		event := &types.Event{
 			Context:     context.Background(),
@@ -460,18 +461,21 @@ func TestDirectTransmission(t *testing.T) {
 	defer dt.Stop()
 
 	now := time.Now().UTC()
+	cfg := &config.MockConfig{
+		TraceIdFieldNames: []string{"trace.trace_id"},
+	}
 
 	// Create events for multiple datasets and scenarios
 	var allEvents []*types.Event
 
 	// Dataset A: 5 events (should create 2 batches: 3 + 2)
 	for i := range 5 {
-		eventData := types.NewPayload(map[string]any{
+		eventData := types.NewPayload(cfg, map[string]any{
 			"trace.trace_id": fmt.Sprintf("trace-a-%d", i),
 			"dataset":        "A",
 			"event_id":       i,
 		})
-		eventData.ExtractMetadata([]string{"trace.trace_id"}, nil)
+		eventData.ExtractMetadata()
 
 		event := &types.Event{
 			Context:     context.Background(),
@@ -502,12 +506,12 @@ func TestDirectTransmission(t *testing.T) {
 
 	// Dataset B: 7 events, one too large (should create 3 batches: 2 + 3 + 1)
 	for i := range 7 {
-		eventData := types.NewPayload(map[string]any{
+		eventData := types.NewPayload(cfg, map[string]any{
 			"trace.trace_id": fmt.Sprintf("trace-b-%d", i),
 			"dataset":        "B",
 			"event_id":       i,
 		})
-		eventData.ExtractMetadata([]string{"trace.trace_id"}, nil)
+		eventData.ExtractMetadata()
 
 		if i == 0 {
 			eventData.Set("huge", strings.Repeat("a", 1024*1024))
@@ -527,13 +531,16 @@ func TestDirectTransmission(t *testing.T) {
 	}
 
 	// Dataset C: 2 events (1 batch via timeout, 1 via stop)
+	mockCfg := &config.MockConfig{
+		TraceIdFieldNames: []string{"trace.trace_id"},
+	}
 	for i := range 2 {
-		eventData := types.NewPayload(map[string]any{
+		eventData := types.NewPayload(mockCfg, map[string]any{
 			"trace.trace_id": fmt.Sprintf("trace-c-%d", i),
 			"dataset":        "C",
 			"event_id":       i,
 		})
-		eventData.ExtractMetadata([]string{"trace.trace_id"}, nil)
+		eventData.ExtractMetadata()
 
 		event := &types.Event{
 			Context:     context.Background(),
@@ -603,7 +610,6 @@ func TestDirectTransmission(t *testing.T) {
 		assert.Equal(t, expectedTraceID, event.Data["trace.trace_id"])
 
 		assert.Equal(t, "trace", event.Data[types.MetaSignalType])
-		assert.Equal(t, expectedTraceID, event.Data[types.MetaTraceID])
 		assert.Equal(t, "span_event", event.Data[types.MetaAnnotationType])
 		assert.Equal(t, true, event.Data[types.MetaRefineryProbe])
 		assert.Equal(t, true, event.Data[types.MetaRefineryRoot])
@@ -686,14 +692,17 @@ func TestDirectTransmissionBatchSizeLimit(t *testing.T) {
 	var allEvents []*types.Event
 
 	bigString := strings.Repeat("a", 700_000)
+	mockCfg := &config.MockConfig{
+		TraceIdFieldNames: []string{"trace.trace_id"},
+	}
 	for i := range 50 {
-		eventData := types.NewPayload(map[string]any{
+		eventData := types.NewPayload(mockCfg, map[string]any{
 			"trace.trace_id": fmt.Sprintf("trace-a-%d", i),
 			"dataset":        "A",
 			"event_id":       i,
 			"big":            bigString,
 		})
-		eventData.ExtractMetadata([]string{"trace.trace_id"}, nil)
+		eventData.ExtractMetadata()
 
 		// Some events are too big to send at all, that shouldn't foul up the logic here.
 		if i == 0 || i == 9 || i == 10 || i == 49 {
@@ -752,6 +761,7 @@ func TestDirectTransmissionBatchTiming(t *testing.T) {
 
 	err := dt.Start()
 	require.NoError(t, err)
+	mockCfg := &config.MockConfig{}
 
 	// Send first event at time 0
 	event1 := &types.Event{
@@ -761,7 +771,7 @@ func TestDirectTransmissionBatchTiming(t *testing.T) {
 		Dataset:    "test-dataset",
 		SampleRate: 1,
 		Timestamp:  fakeClock.Now(),
-		Data:       types.NewPayload(map[string]any{"event": "first", "time": 0}),
+		Data:       types.NewPayload(mockCfg, map[string]any{"event": "first", "time": 0}),
 	}
 	dt.EnqueueEvent(event1)
 
@@ -779,7 +789,7 @@ func TestDirectTransmissionBatchTiming(t *testing.T) {
 		Dataset:    "test-dataset",
 		SampleRate: 1,
 		Timestamp:  fakeClock.Now(),
-		Data:       types.NewPayload(map[string]any{"event": "second", "time": 100}),
+		Data:       types.NewPayload(mockCfg, map[string]any{"event": "second", "time": 100}),
 	}
 	dt.EnqueueEvent(event2)
 
@@ -822,7 +832,7 @@ func TestDirectTransmissionBatchTiming(t *testing.T) {
 		Dataset:    "test-dataset",
 		SampleRate: 1,
 		Timestamp:  fakeClock.Now(),
-		Data:       types.NewPayload(map[string]any{"event": "third", "time": 400}),
+		Data:       types.NewPayload(mockCfg, map[string]any{"event": "third", "time": 400}),
 	}
 	dt.EnqueueEvent(event3)
 
@@ -1011,12 +1021,13 @@ func createBenchmarkEvents(t testing.TB, serverURL string, numEvents, datasets,
 			SampleRate:        uint(1 + (i % 100)),
 			Timestamp:         time.Now().Add(time.Duration(i) * time.Microsecond),
 			EnqueuedUnixMicro: time.Now().UnixMicro(),
+			Data:              types.NewPayload(&config.MockConfig{}, nil),
 		}
 		err := events[i].Data.UnmarshalMsgpack(msgpackPayloads[i%len(msgpackPayloads)])
 		if err != nil {
 			t.Fatalf("failed to unmarshal payload: %v", err)
 		}
-		events[i].Data.ExtractMetadata(nil, nil)
+		events[i].Data.ExtractMetadata()
 	}
 
 	return events
diff --git a/types/event_test.go b/types/event_test.go
index 8f4fb5e..4eb7acc 100644
--- a/types/event_test.go
+++ b/types/event_test.go
@@ -7,6 +7,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/honeycombio/refinery/config"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -33,7 +34,7 @@ func TestSpan_GetDataSize(t *testing.T) {
 			for i := 0; i < tt.numStrings; i++ {
 				data[tt.name+"str"+strconv.Itoa(i)] = strings.Repeat("x", i)
 			}
-			payload := NewPayload(data)
+			payload := NewPayload(&config.MockConfig{}, data)
 			sp := &Span{
 				TraceID: tt.name,
 				Event: Event{
@@ -64,7 +65,7 @@ func TestSpan_GetDataSizeSlice(t *testing.T) {
 			for i := range tt.num {
 				sliceData[i] = i
 			}
-			payload := NewPayload(map[string]any{"data": sliceData})
+			payload := NewPayload(&config.MockConfig{}, map[string]any{"data": sliceData})
 			sp := &Span{
 				Event: Event{
 					Data: payload,
@@ -94,7 +95,7 @@ func TestSpan_GetDataSizeMap(t *testing.T) {
 			for i := range tt.num {
 				mapData[strconv.Itoa(i)] = i
 			}
-			payload := NewPayload(map[string]any{"data": mapData})
+			payload := NewPayload(&config.MockConfig{}, map[string]any{"data": mapData})
 			sp := &Span{
 				Event: Event{
 					Data: payload,
@@ -119,8 +120,8 @@ func TestSpan_AnnotationType(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			payload := NewPayload(tt.data)
-			payload.ExtractMetadata(nil, nil)
+			payload := NewPayload(&config.MockConfig{}, tt.data)
+			payload.ExtractMetadata()
 			sp := &Span{
 				Event: Event{
 					Data: payload,
@@ -134,11 +135,14 @@ func TestSpan_AnnotationType(t *testing.T) {
 }
 
 func TestSpan_ExtractDecisionContext(t *testing.T) {
-	payload := NewPayload(map[string]interface{}{
+	cfg := &config.MockConfig{
+		TraceIdFieldNames: []string{"trace.trace_id"},
+	}
+	payload := NewPayload(cfg, map[string]interface{}{
 		"test":                 "test",
 		"meta.annotation_type": "span_event",
 	})
-	payload.ExtractMetadata(nil, nil)
+	payload.ExtractMetadata()
 	ev := Event{
 		APIHost:     "test.api.com",
 		APIKey:      "test-api-key",
@@ -155,7 +159,7 @@ func TestSpan_ExtractDecisionContext(t *testing.T) {
 		IsRoot:      true,
 	}
 
-	got := sp.ExtractDecisionContext()
+	got := sp.ExtractDecisionContext(cfg)
 	assert.Equal(t, ev.APIHost, got.APIHost)
 	assert.Equal(t, ev.APIKey, got.APIKey)
 	assert.Equal(t, ev.Dataset, got.Dataset)
@@ -163,7 +167,7 @@ func TestSpan_ExtractDecisionContext(t *testing.T) {
 	assert.Equal(t, ev.SampleRate, got.SampleRate)
 	assert.Equal(t, ev.Timestamp, got.Timestamp)
 	expectedData := map[string]interface{}{
-		"meta.trace_id":                sp.TraceID,
+		"trace.trace_id":               sp.TraceID,
 		"meta.refinery.root":           true,
 		"meta.refinery.min_span":       true,
 		"meta.annotation_type":         SpanAnnotationTypeSpanEvent.String(),
@@ -189,8 +193,8 @@ func TestSpan_IsDecisionSpan(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			payload := NewPayload(tt.data)
-			payload.ExtractMetadata(nil, nil)
+			payload := NewPayload(&config.MockConfig{}, tt.data)
+			payload.ExtractMetadata()
 			sp := &Span{
 				Event: Event{
 					Data: payload,
@@ -215,7 +219,7 @@ func BenchmarkSpan_CalculateSizeSmall(b *testing.B) {
 	for i := 0; i < 10; i++ {
 		data["str"+strconv.Itoa(i)] = strings.Repeat("x", i)
 	}
-	payload := NewPayload(data)
+	payload := NewPayload(&config.MockConfig{}, data)
 	sp := &Span{
 		Event: Event{
 			Data: payload,
@@ -235,7 +239,7 @@ func BenchmarkSpan_CalculateSizeLarge(b *testing.B) {
 	for i := 0; i < 500; i++ {
 		data["str"+strconv.Itoa(i)] = strings.Repeat("x", i)
 	}
-	payload := NewPayload(data)
+	payload := NewPayload(&config.MockConfig{}, data)
 	sp := &Span{
 		Event: Event{
 			Data: payload,
diff --git a/types/payload_test.go b/types/payload_test.go
index d30550c..d93a742 100644
--- a/types/payload_test.go
+++ b/types/payload_test.go
@@ -7,6 +7,7 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/honeycombio/refinery/config"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/tinylib/msgp/msgp"
@@ -27,13 +28,16 @@ func TestPayload(t *testing.T) {
 		"key4":   true,
 		"keyNil": nil,
 		// Add some metadata fields to test
-		"meta.trace_id":          "test-trace-123",
+		"trace.trace_id":         "test-trace-123",
 		"meta.refinery.root":     true,
 		"meta.refinery.min_span": false,
 		"meta.annotation_type":   "span_event",
 	}
+	mockCfg := &config.MockConfig{
+		TraceIdFieldNames: []string{"trace.trace_id"},
+	}
 
-	var ph Payload
+	ph := NewPayload(mockCfg, data)
 	doTest := func(t *testing.T) {
 		assert.True(t, ph.Exists("key1"))
 		assert.Equal(t, "value1", ph.Get("key1"))
@@ -105,11 +109,9 @@ func TestPayload(t *testing.T) {
 		asJSON, err := json.Marshal(ph)
 		require.NoError(t, err)
 
-		var fromJSON Payload
+		fromJSON := NewPayload(mockCfg, nil)
 		err = json.Unmarshal(asJSON, &fromJSON)
 		require.NoError(t, err)
-		err = fromJSON.ExtractMetadata(nil, nil)
-		require.NoError(t, err)
 
 		// round-tripping through JSON turns our ints into floats
 		expectedFromJSON := maps.Collect(ph.All())
@@ -118,36 +120,32 @@ func TestPayload(t *testing.T) {
 		assert.EqualValues(t, expectedFromJSON, maps.Collect(fromJSON.All()))
 	}
 
-	ph = NewPayload(data)
-	err := ph.ExtractMetadata(nil, nil)
-	require.NoError(t, err)
+	ph = NewPayload(mockCfg, data)
+	ph.ExtractMetadata()
 	t.Run("from_map", doTest)
 
-	ph = Payload{}
+	ph = NewPayload(mockCfg, nil)
 	msgpData, err := msgpack.Marshal(data)
 	require.NoError(t, err)
 	err = msgpack.Unmarshal(msgpData, &ph)
 	require.NoError(t, err)
-	ph.ExtractMetadata(nil, nil)
 	t.Run("from_msgpack", doTest)
 
 	// Test payload with other stuff (another payload) following.
-	ph = Payload{}
+	ph = NewPayload(mockCfg, nil)
 	extendedMsgpData := append(msgpData, msgpData...)
 	remainder, err := ph.UnmarshalMsg(extendedMsgpData)
 	require.NoError(t, err)
 	assert.Equal(t, msgpData, remainder)
-	ph.ExtractMetadata(nil, nil)
 	t.Run("from_msgp", doTest)
 
 	// Test our own marshaler
 	msgpData, err = ph.MarshalMsg(nil)
 	require.NoError(t, err)
-	ph = Payload{}
+	ph = NewPayload(mockCfg, nil)
 	remainder, err = ph.UnmarshalMsg(msgpData)
 	require.NoError(t, err)
 	assert.Empty(t, remainder)
-	ph.ExtractMetadata(nil, nil)
 	t.Run("from_marshal", doTest)
 }
 
@@ -157,8 +155,10 @@ func TestPayloadExtractMetadataWithFieldNames(t *testing.T) {
 			"trace.trace_id": "custom-trace-123",
 			"service.name":   "test-service",
 		}
-		ph := NewPayload(data)
-		ph.ExtractMetadata([]string{"trace.trace_id", "traceId"}, nil)
+		ph := NewPayload(&config.MockConfig{
+			TraceIdFieldNames: []string{"trace.trace_id"},
+		}, data)
+		ph.ExtractMetadata()
 
 		assert.Equal(t, "custom-trace-123", ph.MetaTraceID, "Should extract trace ID from custom field")
 		assert.Equal(t, "custom-trace-123", ph.Get("trace.trace_id"))
@@ -170,8 +170,11 @@ func TestPayloadExtractMetadataWithFieldNames(t *testing.T) {
 			"trace.trace_id": "trace-123",
 			"span.id":        "span-456",
 		}
-		ph := NewPayload(data)
-		ph.ExtractMetadata([]string{"trace.trace_id"}, []string{"trace.parent_id", "parentId"})
+		ph := NewPayload(&config.MockConfig{
+			TraceIdFieldNames:  []string{"trace.trace_id"},
+			ParentIdFieldNames: []string{"trace.parent_id", "parentId"},
+		}, data)
+		ph.ExtractMetadata()
 
 		assert.Equal(t, "trace-123", ph.MetaTraceID)
 		assert.True(t, ph.MetaRefineryRoot.HasValue, "Root flag should be set")
@@ -183,8 +186,11 @@ func TestPayloadExtractMetadataWithFieldNames(t *testing.T) {
 			"trace.parent_id": "parent-123",
 			"span.id":         "span-789",
 		}
-		ph = NewPayload(data)
-		ph.ExtractMetadata([]string{"trace.trace_id"}, []string{"trace.parent_id", "parentId"})
+		ph = NewPayload(&config.MockConfig{
+			TraceIdFieldNames:  []string{"trace.trace_id"},
+			ParentIdFieldNames: []string{"trace.parent_id", "parentId"},
+		}, data)
+		ph.ExtractMetadata()
 
 		assert.Equal(t, "trace-789", ph.MetaTraceID)
 		assert.True(t, ph.MetaRefineryRoot.HasValue, "Root flag should be set")
@@ -197,8 +203,11 @@ func TestPayloadExtractMetadataWithFieldNames(t *testing.T) {
 			"meta.signal_type":   "log",
 			"trace.trace_id":     "trace-123",
 		}
-		ph := NewPayload(data)
-		ph.ExtractMetadata([]string{"trace.trace_id"}, []string{"trace.parent_id"})
+		ph := NewPayload(&config.MockConfig{
+			TraceIdFieldNames:  []string{"trace.trace_id"},
+			ParentIdFieldNames: []string{"trace.parent_id", "parentId"},
+		}, data)
+		ph.ExtractMetadata()
 
 		assert.Equal(t, "log", ph.MetaSignalType)
 		assert.False(t, ph.MetaRefineryRoot.HasValue, "Root flag should be set")
@@ -213,9 +222,13 @@ func TestPayloadExtractMetadataError(t *testing.T) {
 			msgpMap: MsgpPayloadMap{
 				rawData: []byte{0xFF, 0xFF, 0xFF}, // Invalid msgpack
 			},
+			config: &config.MockConfig{
+				TraceIdFieldNames:  []string{"trace.trace_id"},
+				ParentIdFieldNames: []string{"trace.parent_id", "parentId"},
+			},
 		}
 
-		err := p.ExtractMetadata(nil, nil)
+		err := p.ExtractMetadata()
 		assert.Error(t, err)
 		assert.Contains(t, err.Error(), "failed to read msgpack map header")
 	})
@@ -223,10 +236,10 @@ func TestPayloadExtractMetadataError(t *testing.T) {
 
 func TestPayloadMetaAnnotationType(t *testing.T) {
 	t.Run("handles string values", func(t *testing.T) {
-		ph := NewPayload(map[string]any{
+		ph := NewPayload(&config.MockConfig{}, map[string]any{
 			"meta.annotation_type": "span_event",
 		})
-		err := ph.ExtractMetadata(nil, nil)
+		err := ph.ExtractMetadata()
 		require.NoError(t, err)
 
 		assert.Equal(t, "span_event", ph.MetaAnnotationType)
@@ -235,23 +248,21 @@ func TestPayloadMetaAnnotationType(t *testing.T) {
 
 	t.Run("marshaling preserves type", func(t *testing.T) {
 		// Test string marshaling
-		ph1 := NewPayload(map[string]any{})
+		ph1 := NewPayload(&config.MockConfig{}, nil)
 		ph1.MetaAnnotationType = "span_event"
 
 		msgpData, err := ph1.MarshalMsg(nil)
 		require.NoError(t, err)
 
-		ph2 := Payload{}
+		ph2 := NewPayload(&config.MockConfig{}, nil)
 		_, err = ph2.UnmarshalMsg(msgpData)
 		require.NoError(t, err)
-		err = ph2.ExtractMetadata(nil, nil)
-		require.NoError(t, err)
 
 		assert.Equal(t, "span_event", ph2.MetaAnnotationType)
 	})
 }
 
-func TestPayloadUnmarshalMsgWithMetadata(t *testing.T) {
+func TestPayloadUnmarshalMsg(t *testing.T) {
 	t.Run("extracts metadata during unmarshal", func(t *testing.T) {
 		// Create test data with metadata fields
 		data := map[string]any{
@@ -269,8 +280,13 @@ func TestPayloadUnmarshalMsgWithMetadata(t *testing.T) {
 		require.NoError(t, err)
 
 		// Create payload and unmarshal with metadata extraction
-		var p Payload
-		remainder, err := p.UnmarshalMsgWithMetadata(msgpData, []string{"trace.trace_id"}, []string{"span.parent_id"})
+		p := Payload{
+			config: &config.MockConfig{
+				TraceIdFieldNames:  []string{"trace.trace_id"},
+				ParentIdFieldNames: []string{"span.parent_id"},
+			},
+		}
+		remainder, err := p.UnmarshalMsg(msgpData)
 		require.NoError(t, err)
 		assert.Empty(t, remainder)
 
@@ -288,64 +304,84 @@ func TestPayloadUnmarshalMsgWithMetadata(t *testing.T) {
 	})
 
 	t.Run("handles remainder correctly", func(t *testing.T) {
-		data1 := map[string]any{"meta.trace_id": "trace1"}
-		data2 := map[string]any{"meta.trace_id": "trace2"}
+		data1 := map[string]any{"trace.trace_id": "trace1"}
+		data2 := map[string]any{"trace.trace_id": "trace2"}
 
 		msgpData1, _ := msgpack.Marshal(data1)
 		msgpData2, _ := msgpack.Marshal(data2)
 		combined := append(msgpData1, msgpData2...)
 
-		var p1 Payload
-		remainder, err := p1.UnmarshalMsgWithMetadata(combined, nil, nil)
-		require.NoError(t, err)
+		p1 := Payload{
+			config: &config.MockConfig{
+				TraceIdFieldNames:  []string{"trace.trace_id"},
+				ParentIdFieldNames: []string{"span.parent_id"},
+			},
+		}
+		remainder, err := p1.UnmarshalMsg(combined)
 		assert.Equal(t, "trace1", p1.MetaTraceID)
 		assert.Equal(t, msgpData2, remainder)
 
 		// Verify we can unmarshal the remainder
-		var p2 Payload
-		remainder2, err := p2.UnmarshalMsgWithMetadata(remainder, nil, nil)
+		p2 := Payload{
+			config: &config.MockConfig{
+				TraceIdFieldNames:  []string{"trace.trace_id"},
+				ParentIdFieldNames: []string{"span.parent_id"},
+			},
+		}
+		remainder2, err := p2.UnmarshalMsg(remainder)
 		require.NoError(t, err)
 		assert.Equal(t, "trace2", p2.MetaTraceID)
 		assert.Empty(t, remainder2)
 	})
 }
-
-func TestPayloadGetSetMetadataSync(t *testing.T) {
+func TestPayloadGetSetExistMetadataSync(t *testing.T) {
 	t.Run("Set updates metadata fields", func(t *testing.T) {
-		ph := NewPayload(map[string]any{})
+		ph := NewPayload(&config.MockConfig{}, nil)
 
 		// Test string fields
+		assert.False(t, ph.Exists("meta.signal_type"))
 		ph.Set("meta.signal_type", "trace")
 		assert.Equal(t, "trace", ph.MetaSignalType)
 		assert.Equal(t, "trace", ph.Get("meta.signal_type"))
+		assert.True(t, ph.Exists("meta.signal_type"))
 
+		assert.False(t, ph.Exists("meta.trace_id"))
 		ph.Set("meta.trace_id", "test-trace-456")
 		assert.Equal(t, "test-trace-456", ph.MetaTraceID)
 		assert.Equal(t, "test-trace-456", ph.Get("meta.trace_id"))
+		assert.True(t, ph.Exists("meta.trace_id"))
 
 		// Test boolean fields
+		assert.False(t, ph.Exists("meta.refinery.probe"))
 		ph.Set("meta.refinery.probe", true)
 		assert.True(t, ph.MetaRefineryProbe.HasValue)
 		assert.True(t, ph.MetaRefineryProbe.Value)
 		assert.Equal(t, true, ph.Get("meta.refinery.probe"))
+		assert.True(t, ph.Exists("meta.refinery.probe"))
 
+		assert.False(t, ph.Exists("meta.refinery.root"))
 		ph.Set("meta.refinery.root", false)
 		assert.True(t, ph.MetaRefineryRoot.HasValue)
 		assert.False(t, ph.MetaRefineryRoot.Value)
 		assert.Equal(t, false, ph.Get("meta.refinery.root"))
+		assert.True(t, ph.Exists("meta.refinery.root"))
 
 		// Test int64 fields
+		assert.False(t, ph.Exists("meta.refinery.send_by"))
 		ph.Set("meta.refinery.send_by", int64(12345))
 		assert.Equal(t, int64(12345), ph.MetaRefinerySendBy)
 		assert.Equal(t, int64(12345), ph.Get("meta.refinery.send_by"))
+		assert.True(t, ph.Exists("meta.refinery.send_by"))
 
+		assert.False(t, ph.Exists("meta.refinery.span_data_size"))
 		ph.Set("meta.refinery.span_data_size", int64(67890))
 		assert.Equal(t, int64(67890), ph.MetaRefinerySpanDataSize)
 		assert.Equal(t, int64(67890), ph.Get("meta.refinery.span_data_size"))
+		assert.True(t, ph.Exists("meta.refinery.span_data_size"))
 	})
 
 	t.Run("Get returns from dedicated fields", func(t *testing.T) {
-		ph := NewPayload(map[string]any{})
+		ph := NewPayload(&config.MockConfig{}, nil)
 
 		// Set fields directly
 		ph.MetaSignalType = "log"
@@ -360,16 +396,30 @@ func TestPayloadGetSetMetadataSync(t *testing.T) {
 		assert.Equal(t, true, ph.Get("meta.refinery.probe"))
 		assert.Equal(t, false, ph.Get("meta.refinery.root"))
 		assert.Equal(t, int64(54321), ph.Get("meta.refinery.send_by"))
+
+		// Exists should also work when fields are set directly
+		assert.True(t, ph.Exists("meta.signal_type"))
+		assert.True(t, ph.Exists("meta.trace_id"))
+		assert.True(t, ph.Exists("meta.refinery.probe"))
+		assert.True(t, ph.Exists("meta.refinery.root"))
+		assert.True(t, ph.Exists("meta.refinery.send_by"))
 	})
 
 	t.Run("Get returns nil for unset boolean fields", func(t *testing.T) {
-		ph := NewPayload(map[string]any{})
+		ph := NewPayload(&config.MockConfig{}, nil)
 
-		// Boolean fields without values should return nil
+		// Boolean fields without values should return nil and not exist
 		assert.Nil(t, ph.Get("meta.refinery.probe"))
+		assert.False(t, ph.Exists("meta.refinery.probe"))
+
 		assert.Nil(t, ph.Get("meta.refinery.root"))
+		assert.False(t, ph.Exists("meta.refinery.root"))
+
 		assert.Nil(t, ph.Get("meta.refinery.min_span"))
+		assert.False(t, ph.Exists("meta.refinery.min_span"))
+
 		assert.Nil(t, ph.Get("meta.refinery.expired_trace"))
+		assert.False(t, ph.Exists("meta.refinery.expired_trace"))
 	})
 }
 
@@ -410,11 +460,14 @@ func TestUnmarshalMsgWithMetadata(t *testing.T) {
 	}
 
 	// Test the optimized unmarshal
-	var p Payload
-	traceIdFields := []string{"trace.trace_id", "traceId"}
-	parentIdFields := []string{"trace.parent_id", "parentId"}
+	p := Payload{
+		config: &config.MockConfig{
+			TraceIdFieldNames:  []string{"trace.trace_id", "traceId"},
+			ParentIdFieldNames: []string{"span.parent_id", "parentId"},
+		},
+	}
 
-	remaining, err := p.UnmarshalMsgWithMetadata(buf, traceIdFields, parentIdFields)
+	remaining, err := p.UnmarshalMsg(buf)
 	require.NoError(t, err)
 	assert.Empty(t, remaining)
 
@@ -460,31 +513,29 @@ func BenchmarkPayload(b *testing.B) {
 	msgpData, err := msgpack.Marshal(data)
 	require.NoError(b, err)
 
-	phMap := NewPayload(data)
-	phMap.ExtractMetadata(nil, nil)
-	var phMsgp Payload
+	phMap := NewPayload(&config.MockConfig{}, data)
+	phMsgp := NewPayload(&config.MockConfig{}, nil)
 	err = msgpack.Unmarshal(msgpData, &phMsgp)
 	require.NoError(b, err)
-	phMsgp.ExtractMetadata(nil, nil)
 
 	b.Run("create_map", func(b *testing.B) {
 		for b.Loop() {
 			var m map[string]any
 			_ = msgpack.Unmarshal(msgpData, &m)
-			_ = NewPayload(m)
+			_ = NewPayload(&config.MockConfig{}, m)
 		}
 	})
 
 	b.Run("create_msgpack", func(b *testing.B) {
 		for b.Loop() {
-			var phMsgp Payload
+			phMsgp := NewPayload(&config.MockConfig{}, nil)
 			_ = phMsgp.UnmarshalMsgpack(msgpData)
 		}
 	})
 
 	b.Run("create_msgp", func(b *testing.B) {
 		for b.Loop() {
-			var phMsgp Payload
+			phMsgp := NewPayload(&config.MockConfig{}, nil)
 			_, _ = phMsgp.UnmarshalMsg(msgpData)
 		}
 	})
@@ -516,7 +567,7 @@ func BenchmarkPayload(b *testing.B) {
 			modulo := len(keys) - num
 			for n := range b.N {
 				offset := n % modulo
-				var phMsgpMemo Payload
+				phMsgpMemo := NewPayload(&config.MockConfig{}, nil)
 				_ = phMsgpMemo.UnmarshalMsgpack(msgpData)
 				phMsgpMemo.MemoizeFields(keys[offset : offset+num]...)
 				for _, key := range keys[offset : offset+num] {