Data Structure

This guide outlines how NotifyVisitors structures and manages events and user profiles, the core building blocks of your analytics data model. You’ll learn about the different attribute types (system, default, and custom), how they’re captured, and best practices for designing a scalable schema that remains consistent as your product evolves.

In data warehouse parlance, events represent your fact tables — the actions users take — while user profiles serve as dimension tables, providing context about who those users are. Together, they form a reliable foundation for accurate tracking, analysis, and personalization across your product.

User attributes

user profile represents a collection of properties associated with an individual user. It functions as a key–value store that maintains the current state of that user. User profiles are connected to events through a distinct id (nv_uid): event.nv_uid = user.nv_uid.

In NotifyVisitors, user profiles bring together system-generated identifiers, demographic details, and custom-defined attributes. At a minimum, each profile includes a distinct identifier (such as user_idemail, or mobile).

How User Profiles Are Stored

Behind the scenes, NotifyVisitors organizes user data in a structured table. Each row represents a single user profile, while columns represent user attributes (e.g., Name, Email, Department). These attributes can be updated as new information becomes available.

nv_uidNameEmailDepartment
101David[email protected]Engineering
202Emma[email protected]Product
303Liam[email protected]Design

Concepts

  • System attributes:
    Core identifiers and reserved fields such as nv_uidname / first_name / last_nameemailmobile, and first/last seen timestamps.
  • Default attributes:
    Automatically derived and maintained metadata, including CityCountryRegion CodeTime Zone, etc., which are updated whenever a profile changes.
  • Custom attributes:
    Business-specific traits that extend profile context — such as planaccount_tier, or LTV — allowing deeper segmentation and personalization beyond system and default fields.

Event attributes

Events represent actions that occur within your product. Each event includes details describing that action — such as time, device, browser, and location. At minimum, every event should include:

  • Event name
  • Timestamp
  • Distinct identifier (such as user_idemail, or mobile)

Events can also be joined with user profiles to provide additional context and enrichment.

Concepts

System attributes:

These are automatically captured when an event occurs. They include information like device, operating system, IP-based location, language, platform, timezone, UTM parameters, and page or app context. Coverage varies by platform (Web, Android, iOS).

Custom attributes:

These are business-defined details that you add to specific events — for example, order_idplan_tier, or quantity.

  • If you think in database terms: events are like tables, and attributes are like columns.
  • If you’re familiar with Google Analytics: events are like hits, and attributes are like dimensions.

Examples

  • Page Viewed event might include an attribute called Page URL, representing the page that was viewed.
  • Signed Up event could include Signup Type, showing whether the signup was organic or referral.
  • Song Played event might include Song Name, containing the name of the song.
  • An Order Completed event could include Items, which lists objects describing each item (name, category, price, etc.).

Use Cases

You can filter, group, and analyze events using their attributes to answer questions such as:

  • Which pages do users visit before the pricing page?
  • How many sign-ups came from organic vs. referral sources?
  • Which song is played most often?
  • How many orders included shoes, and what was the total amount spent on them last month?

Best Practices (Keep Events Action-Oriented)

Define events around user actions rather than being too broad or too narrow. Use event properties to add context, instead of creating multiple events that describe similar actions.

Examples:

  • To analyze page navigation:
    Instead of separate events like Home Page Viewed and Pricing Page Viewed, track a single Page Viewed event with a Page Name property (e.g., “/home”, “/pricing”).
  • To track items added to a cart:
    Instead of Add Shirt to CartAdd Hoodie to Cart, and Add Socks to Cart, use one Add to Cart event with an Item property (e.g., “Shirt”, “Hoodie”, “Socks”).
  • To track button interactions on a single screen:
    Instead of Blue Button Clicked and Checkout Button Clicked, record one Button Clicked event with Color(“Blue”) and Button Name (“Checkout”).
  • To capture different user actions:
    Instead of one generic Button Clicked event for everything (with Button Name = “Play”, “Profile”, “X”), create distinct events like Song PlayedProfile Updated, and Logout, each with properties specific to their context.

Critical Guidelines

We don’t impose a limit on the total number of events you can send to NotifyVisitors, but it will be considered in your pricing. However, we have a soft limit of 1000 distinct event names. If you send more event names, we’ll still process them, but those event names won’t be indexed and won’t appear in our autocomplete menus.

  • Each event can have up to 255 properties.
  • Event names are case-sensitive and must be < 255 characters.
    sign_up_completed and Sign_Up_Completed are different events.
  • Custom event attribute names are also case-sensitive and must be < 50 characters.
  • String attribute values must be < 512 characters.
  • Per-event attribute fan-out: on a single event, you can include ≤ 25 attributes per data type (e.g., at most 25 string-type attributes, 25 number-type attributes, etc.).
  • Type locking: the first value seen for an attribute establishes its type. If you later send a different type (e.g., quantity: "five" after sending quantity: 5), those mismatched values won’t flow to your NotifyVisitors dashboard.
    Tip: In such cases, introduce a new attribute like quantity_v2 with the correct type; do not change the original type.

Naming conventions that survive the long run

Because keys are case-sensitive and hard to rename, choose a durable scheme: Use snake_case for event names and property names.

It’s resilient in code, storage, and BI tools.

  • Use object_verb for events: page_viewedsong_playedorder_completed.

  • Keep names short but specific: prefer signup_type over type.

  • If you need friendlier labels for the UI, NotifyVisitors allows you to set display names for analytics reports without changing the underlying keys.

Property data types

Selecting the appropriate data type for properties is fundamental to effective data analysis. Here is a detailed breakdown of the property data types, their characteristics, and how they behave in an analytics context.

String

A String is an alphanumeric value and serves as the default data type for any property value that doesn't conform to other types.

  • Description: Represents text-based data.
  • Examples: User ID = “Bruno_21”, Status = “Active”.
  • Limits: Each string is limited to 255 bytes. The number of characters this accommodates depends on the character encoding used.

Numeric

A Numeric property can be either an integer or a decimal. This type is essential for any quantitative analysis.

  • Description: Represents numerical data.
  • Examples: Cost = 15.00, Quantity = 5
  • Functionality: Enables the use of mathematical operators such as sum, median, and percentile in reports.

Boolean

A Boolean property represents a true or false value.

  • Description: Represents binary states.
  • Examples: true, false.
  • Typecasting: Non-boolean values are automatically converted. For instance, 0, null, undefined, and empty strings are treated as false, while any non-zero or non-empty values are treated as true.

Date

A Date stores a calendar date only (no time, no timezone).

  • Description: Day on the calendar. Use ISO-8601 date-only format.
  • Examples: renewal_date = "2025-12-01"dob = "1998-07-14".
  • Limits: Does not include time or timezone. Format YYYY-MM-DD. Keep consistent across sources.

Timestamp

A Timestamp stores an exact point in time and includes timezone.

  • Description: The Moment an action occurred. Use ISO-8601 with timezone (prefer UTC Z).
  • Examples: timestamp = "2025-09-10T12:34:56Z"client_timestamp = "2025-09-10T18:04:56+05:30".
  • Limits: Must include timezone (Z or ±HH:MM). Avoid locale strings (e.g., "09/10/25 12:34 PM").

Object

An Object is a key–value map grouping related attributes.

  • Description: Structured container (e.g., address, device capabilities).
  • Example:
{
  "shipping_address": {
    "street": "42 MG Road",
    "city": "Mumbai",
    "zip": "400001",
    "country_code": "IN"
  }
}
  • Limits: Less than 50 characters. Counts as one Object-type attribute toward per-type limits; inner strings still follow string limits.

Nested Object

A Nested Object is an array of multiple objects.

  • Description: Use when a group contains another group (e.g., payment → card details).
  • Example:
    {
      "payment": {
        "method": "credit_card",
        "amount": 119.93,
        "card": {
          "brand": "Visa",
          "last4": "4242",
          "exp_month": 12,
          "exp_year": 2030
        }
      }
    }
    
  • Limits: Event property: 8 KB; User profile property: 255 characters; up to 25 keys per nested object with a max nesting depth of 3.

Array

An Array is an ordered list of values (e.g., strings or numbers).

  • Description: Use for tags, categories, or line items.

  • Example:

    {
      "tags": ["summer-sale", "new-arrival", "bestseller"],
      "applied_discounts": ["SUMMER20", "FIRST10"]
    }
    
    
  • Limits

    • Each item obeys data-type limits: e.g., strings ≤ 255 characters.

Array of Objects

An Array of Objects is a list where each element is an object containing key-value pairs.

Use this format when you need to store multiple structured records, such as product items, transactions, or users.

  • Description: Use when each item requires multiple attributes (e.g., skunamepriceqty).
  • Example:
{
  "items": [
    { "sku": "SN-001", "name": "AirStride Pro", "price": 99.95, "qty": 1 },
    { "sku": "SK-042", "name": "Cotton Ankle Socks", "price": 9.99, "qty": 2 }
  ]
}
  • Limits

    • Each item obeys data-type limits: e.g., strings ≤ 255 characters.
    • An array of objects can’t be nested. For example:
      Not allowed: object nesting inside array of objects
    {
      "items": [
        {
          "sku": "SN-001",
          "name": "AirStride Pro",
          "price": 99.95,
          "qty": 1,
          "tags": [
            { "tag": "summer-sale" },
            { "tag": "clearance" }
          ]
        },
        {
          "sku": "SK-042",
          "name": "Cotton Ankle Socks",
          "price": 9.99,
          "qty": 2,
          "tags": [
            { "tag": "cotton" },
            { "tag": "bundle" }
          ]
        }
      ]
    }
    

    Allowed option A: make inner arrays strings

    {
      "items": [
        {
          "sku": "SN-001",
          "name": "AirStride Pro",
          "price": 99.95,
          "qty": 1,
          "tags": ["summer-sale", "clearance"]
        },
        {
          "sku": "SK-042",
          "name": "Cotton Ankle Socks",
          "price": 9.99,
          "qty": 2,
          "tags": ["cotton", "bundle"]
        }
      ]
    }
    

    Allowed option B: split into a separate array of objects (no inner arrays)

    {
      "items": [
        { "sku": "SN-001", "name": "AirStride Pro", "price": 99.95, "qty": 1 },
        { "sku": "SK-042", "name": "Cotton Ankle Socks", "price": 9.99, "qty": 2 }
      ],
      "item_tags": [
        { "sku": "SN-001", "tag": "summer-sale" },
        { "sku": "SN-001", "tag": "clearance" },
        { "sku": "SK-042", "tag": "cotton" },
        { "sku": "SK-042", "tag": "bundle" }
      ]
    }