Respectlytics Respect lytics
Menu
Architecture Data Minimization Privacy Engineering Opinion

Why We Killed Custom Event Properties
(And Why You Should Thank Us)

8 min read

Every other analytics SDK competes on "flexibility"—more custom properties, more event types, more data. We went the other direction. We architecturally block custom properties to prevent accidental PII leaks. Most developers think we're crazy. We think we're saving them from a lawsuit.

Every major analytics platform—Mixpanel, Amplitude, Firebase, PostHog—has a feature they're proud of: custom event properties. Track any event. Attach any data. Infinite flexibility.

We removed that feature entirely. Our SDK doesn't have a properties parameter. Our API rejects any data beyond 5 predefined fields. And we think this makes us better, not worse.

This isn't a limitation—it's a safety feature. Here's why.

🪤 The "Infinite Attributes" Trap

Custom properties feel like a superpower when you're building. Need to segment users? Add a property. Want to track purchase amounts? Add a property. Debugging an edge case? Log the user's input as a property.

The problem isn't today. The problem is 18 months from now when:

  • A junior developer adds track("feedback", {message: userInput})—and userInput contains email addresses, phone numbers, or worse
  • Someone debugging a payment flow adds track("payment_error", {debug: paymentResponse})—and paymentResponse contains credit card tokens
  • A health app tracks track("symptom_selected", {symptom: userSymptom})—congratulations, you're now storing protected health information in your analytics database
  • An A/B test logs track("variant_shown", {userId: user.id})—and now your "anonymous" analytics can be linked to individual users

Every custom property is a door you're leaving unlocked. Most of the time, nothing walks through. But when it does, you have a data spill—and potentially a regulatory incident.

💀 A Real Horror Story

Here's a scenario that's happened at companies you've heard of:

The Timeline of a Data Spill

Day 1: Developer adds track("purchase", {note: orderNote}) to help debug a checkout issue. Ships to production.
Month 3: Original developer leaves the company. Nobody remembers the debug code.
Month 14: Privacy audit discovers that orderNote sometimes contains delivery instructions like "Ring bell for John Smith at 555-1234" or "Leave at back door, gate code 9876".
Month 14 + 1h: Legal confirms this is personal data. You've been collecting it for 14 months without disclosure, retention policy, or deletion procedures.
Month 14 + 1w: You're now scrubbing backups, updating privacy policies, and possibly notifying regulators—depending on your jurisdiction.

The flexibility of custom properties enabled this. The developer wasn't malicious—they were debugging. But the architecture allowed a debug statement to become a 14-month data collection program.

🏗️ The Architecture of Constraint

Our approach is different. We enforce data minimization at the API level—not as a policy, but as an architectural constraint.

The Strict 5-Field Model

Every event we store has exactly 5 fields. No more, no less:

  1. event_name — What happened ("upgrade_button_clicked")
  2. session_id — Anonymous session identifier (RAM-only, rotates every 2 hours)
  3. timestamp — When it happened
  4. platform — iOS or Android
  5. country — Derived from IP (IP is immediately discarded)

That's it. The API rejects any additional data. The SDK doesn't have a properties parameter. You can't accidentally leak PII because there's nowhere to put it.

What This Looks Like in Code

Traditional Analytics SDK (The Risk)

// Nothing stops you from doing this:
Analytics.track("signup", properties: [
    "email": user.email,           // ✅ Sent (and now stored forever)
    "name": user.fullName,          // ✅ Sent (PII in your analytics)
    "debug": user.toString()        // ✅ Sent (who knows what's in there)
])

Respectlytics SDK (The Guardrail)

// No properties parameter exists:
Respectlytics.track("signup_completed")  // ✅ That's it. Nothing else to add.

// This doesn't compile:
Respectlytics.track("signup", properties: [...])  // ❌ Error: extra argument 'properties'

// Even if you hit the API directly with extra fields:
// POST /api/v1/track
// {"event_name": "signup", "email": "[email protected]"}
// → API accepts request but silently discards "email"

🛡️ Defensible by Default

We call our philosophy Return of Avoidance (ROA)—the best way to handle sensitive data is to never collect it. But the real payoff isn't philosophical. It's practical.

The Audit Question You Never Want to Hear

Imagine an auditor—internal, external, or regulatory—asks:

"Can you confirm that no personal data has been collected in your analytics events over the past 3 years?"

With traditional analytics, answering this requires:

  • Auditing every track() call in your codebase
  • Reviewing 3 years of git history for property changes
  • Scanning your analytics database for patterns that look like emails, phone numbers, or addresses
  • Hoping no contractor or intern added a debug statement you missed

With architectural constraints, the answer is simpler:

"Yes. It's technically impossible."

Our API only accepts 5 predefined fields. Custom properties are rejected at the API level. Here's the documentation. Here's the API response for rejected fields. There's no code review required—the architecture enforces the policy.

This is what defensible by design means. Not a policy that developers might follow. An architecture that makes violations impossible.

📊 How to Measure Intent Without Identity

The first objection we hear: "But how do I segment users without custom properties?"

The answer: use descriptive event names instead of generic events with properties.

Event Naming Taxonomy (The Pattern)

❌ Traditional (Risky) ✅ Respectlytics (Safe)
track("click", {button: "upgrade"}) track("upgrade_button_clicked")
track("purchase", {plan: "pro"}) track("pro_plan_purchased")
track("feature_used", {user_type: "premium"}) track("premium_user_feature_x_used")
track("error", {code: 500, page: "/checkout"}) track("checkout_error_500")
track("search", {query: userQuery}) track("search_performed")

Notice the last row. You lose the search query—intentionally. Because userQuery might be "pharmacy near 123 Main Street" or "Dr. Smith appointment." That's the kind of data that becomes a liability.

You still know searches are happening. You know how many. You know which platform. You know the trend over time. You just don't know what people searched for—and you probably didn't need to.

⚔️ The Counter-Arguments (And Our Responses)

"But I need custom properties for debugging!"

Use a logging system for debugging—not analytics. Logs can have short retention, access controls, and PII scrubbing. Analytics is a long-term data warehouse. It's the wrong place for debug data.

"What if I'm careful about what I track?"

You might be. But can you guarantee every developer on your team, for the next 5 years, will be equally careful? Policies fail. Architecture doesn't.

"This limits what I can analyze!"

Yes. That's the point. We help you analyze behavior (what features are used, where users drop off, conversion rates) without analyzing individuals. If you need user-level analytics, you need a different tool—and a more complex privacy posture.

"Our privacy team reviews all tracking changes."

Great! How many PRs did they review last month? How confident are you that the intern's hotfix at 2 AM was caught? Architectural constraints work 24/7, never get tired, and never miss a code path.

Frequently Asked Questions

Q: Can I track purchase amounts or revenue?

Not as properties. But you can use event naming: purchase_tier_1, purchase_tier_2, purchase_tier_3. You get tier distribution without exact amounts. If you need exact revenue tracking, that belongs in your billing system, not your analytics.

Q: How do I track A/B test variants?

Embed the variant in the event name: checkout_v2_completed vs checkout_v1_completed. You can compare conversion rates between variants without storing which user saw which variant.

Q: What about user segmentation (free vs paid users)?

Use event prefixes: free_user_feature_x vs paid_user_feature_x. Your client knows if the user is free or paid—encode that knowledge in the event name, not a property that could also carry other data.

Q: Doesn't this create event name explosion?

It creates intentional event names. Traditional analytics creates event name simplicity with property explosion. We're just being explicit about what you're tracking instead of hiding it in unstructured properties.

💡 Key Takeaways

  • Custom properties are an uncontrolled surface for PII leaks—every property is a door you're leaving unlocked
  • Architectural constraints beat policies—code review fails; API rejection doesn't
  • Defensible by default—answer "Do you collect PII?" with documentation, not database scans
  • Event naming taxonomy replaces properties—you lose flexibility, gain safety

Legal Disclaimer

This article provides architectural recommendations for privacy-conscious analytics design. It does not constitute legal advice. Privacy requirements vary by jurisdiction and change over time. Consult your legal team to determine the requirements that apply to your situation.

Additional Resources

Ready to try analytics without the liability?

See what you can measure with just 5 fields. No custom properties, no PII surface area.