User Properties & Segmentation: Custom Dimensions, Cohorts & Attributes

Understanding who your ChatGPT app users are is just as important as tracking what they do. User properties and segmentation transform raw usage data into actionable insights that enable personalized experiences, targeted engagement, and data-driven product decisions.

While events tell you what happened, user properties tell you who made it happen. By enriching user profiles with custom properties, creating cohorts based on behavior patterns, and building targeted audiences, you can deliver ChatGPT experiences that adapt to each user's context, preferences, and journey stage.

This comprehensive guide covers the complete user segmentation ecosystem: custom user properties for profile enrichment, GA4 custom dimensions for analytics integration, cohort analysis for retention tracking, audience building for targeted campaigns, attribute enrichment from third-party sources, and personalization engines that adapt experiences in real-time. Whether you're building a fitness coaching ChatGPT app that personalizes workouts based on user fitness levels or a restaurant reservation app that segments users by dining preferences, this guide provides production-ready implementations for sophisticated user segmentation.

Let's explore how to transform anonymous ChatGPT users into rich, segmented profiles that power personalized experiences and drive engagement.

Custom User Properties: Building Rich User Profiles

User properties are persistent attributes that describe user characteristics, preferences, and states. Unlike event parameters that capture momentary context, user properties remain attached to the user profile and evolve over time as you learn more about each user.

Effective user properties span multiple categories: demographic properties (age range, location, language), behavioral properties (engagement level, feature usage patterns, activity frequency), preference properties (communication channels, content topics, interaction styles), lifecycle properties (signup date, subscription tier, onboarding status), and calculated properties (lifetime value, risk score, engagement score).

Implementing the User Property Manager

The user property manager handles property validation, persistence, analytics integration, and change tracking:

// user-property-manager.ts - Production user property management system
import { getAnalytics, setUserProperties } from 'firebase/analytics';
import { getFirestore, doc, setDoc, getDoc, updateDoc, serverTimestamp } from 'firebase/firestore';

interface UserProperty {
  name: string;
  value: string | number | boolean;
  type: 'demographic' | 'behavioral' | 'preference' | 'lifecycle' | 'calculated';
  source: 'user_input' | 'computed' | 'enriched' | 'inferred';
  updatedAt?: Date;
}

interface UserProfile {
  userId: string;
  properties: Record<string, UserProperty>;
  segments: string[];
  cohorts: string[];
  createdAt: Date;
  lastUpdatedAt: Date;
}

export class UserPropertyManager {
  private analytics = getAnalytics();
  private firestore = getFirestore();
  private propertyCache: Map<string, UserProfile> = new Map();
  private readonly PROPERTY_LIMITS = {
    maxProperties: 25, // GA4 limit
    maxPropertyNameLength: 40,
    maxPropertyValueLength: 100
  };

  /**
   * Set user properties with validation and persistence
   */
  async setUserProperties(
    userId: string,
    properties: Record<string, UserProperty>
  ): Promise<void> {
    try {
      // Validate properties
      this.validateProperties(properties);

      // Get current profile
      const profile = await this.getUserProfile(userId);

      // Merge new properties
      const updatedProperties = {
        ...profile.properties,
        ...properties
      };

      // Update profile
      const updatedProfile: UserProfile = {
        ...profile,
        properties: updatedProperties,
        lastUpdatedAt: new Date()
      };

      // Persist to Firestore
      await this.persistProfile(userId, updatedProfile);

      // Sync to Firebase Analytics (GA4-compatible properties only)
      await this.syncToAnalytics(userId, properties);

      // Update cache
      this.propertyCache.set(userId, updatedProfile);

      console.log('[UserPropertyManager] Properties set:', {
        userId,
        propertyCount: Object.keys(properties).length
      });
    } catch (error) {
      console.error('[UserPropertyManager] Failed to set properties:', error);
      throw error;
    }
  }

  /**
   * Get user profile with all properties
   */
  async getUserProfile(userId: string): Promise<UserProfile> {
    // Check cache first
    if (this.propertyCache.has(userId)) {
      return this.propertyCache.get(userId)!;
    }

    // Load from Firestore
    const profileRef = doc(this.firestore, 'userProfiles', userId);
    const profileSnap = await getDoc(profileRef);

    if (profileSnap.exists()) {
      const profile = profileSnap.data() as UserProfile;
      this.propertyCache.set(userId, profile);
      return profile;
    }

    // Return default profile
    return {
      userId,
      properties: {},
      segments: [],
      cohorts: [],
      createdAt: new Date(),
      lastUpdatedAt: new Date()
    };
  }

  /**
   * Validate property constraints
   */
  private validateProperties(properties: Record<string, UserProperty>): void {
    const propertyCount = Object.keys(properties).length;
    if (propertyCount > this.PROPERTY_LIMITS.maxProperties) {
      throw new Error(`Cannot set more than ${this.PROPERTY_LIMITS.maxProperties} properties`);
    }

    for (const [name, property] of Object.entries(properties)) {
      if (name.length > this.PROPERTY_LIMITS.maxPropertyNameLength) {
        throw new Error(`Property name "${name}" exceeds ${this.PROPERTY_LIMITS.maxPropertyNameLength} characters`);
      }

      const valueStr = String(property.value);
      if (valueStr.length > this.PROPERTY_LIMITS.maxPropertyValueLength) {
        throw new Error(`Property value for "${name}" exceeds ${this.PROPERTY_LIMITS.maxPropertyValueLength} characters`);
      }
    }
  }

  /**
   * Persist profile to Firestore
   */
  private async persistProfile(userId: string, profile: UserProfile): Promise<void> {
    const profileRef = doc(this.firestore, 'userProfiles', userId);
    await setDoc(profileRef, {
      ...profile,
      lastUpdatedAt: serverTimestamp()
    }, { merge: true });
  }

  /**
   * Sync properties to Firebase Analytics
   */
  private async syncToAnalytics(
    userId: string,
    properties: Record<string, UserProperty>
  ): Promise<void> {
    // Convert to GA4-compatible format (only string values)
    const analyticsProperties: Record<string, string> = {};

    for (const [name, property] of Object.entries(properties)) {
      analyticsProperties[name] = String(property.value);
    }

    setUserProperties(this.analytics, analyticsProperties);
  }
}

This user property manager provides validated property setting, multi-tier persistence (cache + Firestore + GA4), property type categorization, and source tracking for compliance and debugging.

Custom Dimensions: GA4 Integration for Analytics

Custom dimensions extend GA4's standard reporting with ChatGPT-specific user attributes. While user properties enrich individual profiles, custom dimensions make those attributes queryable in GA4 reports, enabling cohort analysis, funnel segmentation, and attribution modeling.

Building the Custom Dimension Tracker

The custom dimension tracker manages GA4 dimension registration, value formatting, and reporting integration:

// custom-dimension-tracker.ts - GA4 custom dimension integration
import { getAnalytics, logEvent } from 'firebase/analytics';

interface CustomDimension {
  name: string;
  scope: 'user' | 'event';
  description: string;
  parameterName: string;
  registeredInGA4: boolean;
}

export class CustomDimensionTracker {
  private analytics = getAnalytics();
  private dimensions: Map<string, CustomDimension> = new Map();

  constructor() {
    this.registerDefaultDimensions();
  }

  /**
   * Register default ChatGPT app dimensions
   */
  private registerDefaultDimensions(): void {
    // User-scoped dimensions
    this.registerDimension({
      name: 'user_tier',
      scope: 'user',
      description: 'User subscription tier',
      parameterName: 'user_tier',
      registeredInGA4: true
    });

    this.registerDimension({
      name: 'signup_source',
      scope: 'user',
      description: 'User acquisition source',
      parameterName: 'signup_source',
      registeredInGA4: true
    });

    this.registerDimension({
      name: 'feature_usage_level',
      scope: 'user',
      description: 'Feature adoption tier (power|regular|casual)',
      parameterName: 'feature_usage_level',
      registeredInGA4: true
    });

    // Event-scoped dimensions
    this.registerDimension({
      name: 'interaction_type',
      scope: 'event',
      description: 'ChatGPT interaction category',
      parameterName: 'interaction_type',
      registeredInGA4: true
    });

    this.registerDimension({
      name: 'tool_complexity',
      scope: 'event',
      description: 'Tool call complexity score',
      parameterName: 'tool_complexity',
      registeredInGA4: true
    });
  }

  /**
   * Register custom dimension
   */
  registerDimension(dimension: CustomDimension): void {
    this.dimensions.set(dimension.name, dimension);
    console.log('[CustomDimensionTracker] Registered dimension:', dimension.name);
  }

  /**
   * Track event with custom dimensions
   */
  trackWithDimensions(
    eventName: string,
    dimensions: Record<string, string | number>,
    eventParams?: Record<string, any>
  ): void {
    const params: Record<string, any> = { ...eventParams };

    // Add dimension values
    for (const [dimensionName, value] of Object.entries(dimensions)) {
      const dimension = this.dimensions.get(dimensionName);
      if (!dimension) {
        console.warn(`[CustomDimensionTracker] Unknown dimension: ${dimensionName}`);
        continue;
      }

      params[dimension.parameterName] = String(value);
    }

    logEvent(this.analytics, eventName, params);
  }

  /**
   * Set user-scoped dimensions
   */
  setUserDimensions(dimensions: Record<string, string | number>): void {
    const userDimensions: Record<string, any> = {};

    for (const [name, value] of Object.entries(dimensions)) {
      const dimension = this.dimensions.get(name);
      if (dimension?.scope === 'user') {
        userDimensions[dimension.parameterName] = String(value);
      }
    }

    // Track as user_properties_set event
    logEvent(this.analytics, 'user_properties_set', userDimensions);
  }

  /**
   * Get dimension configuration guide
   */
  getGA4ConfigurationGuide(): string {
    let guide = '=== GA4 Custom Dimension Configuration ===\n\n';
    guide += 'Navigate to: GA4 Admin > Data display > Custom definitions\n\n';

    for (const [name, dimension] of this.dimensions) {
      guide += `Dimension: ${dimension.name}\n`;
      guide += `  Scope: ${dimension.scope}\n`;
      guide += `  Parameter: ${dimension.parameterName}\n`;
      guide += `  Description: ${dimension.description}\n\n`;
    }

    return guide;
  }
}

// Usage example
const dimensionTracker = new CustomDimensionTracker();

// Set user-scoped dimensions
dimensionTracker.setUserDimensions({
  user_tier: 'professional',
  signup_source: 'chatgpt_store',
  feature_usage_level: 'power'
});

// Track event with event-scoped dimensions
dimensionTracker.trackWithDimensions('tool_call_completed', {
  interaction_type: 'data_retrieval',
  tool_complexity: 'high'
}, {
  tool_name: 'getCustomerData',
  duration_ms: 1250
});

This custom dimension tracker ensures GA4 compatibility, provides clear configuration documentation, and enables rich segmentation in GA4 reports and explorations.

Cohort Analysis: Tracking User Groups Over Time

Cohort analysis groups users by shared characteristics (signup date, acquisition source, feature adoption) and tracks their behavior over time. This reveals retention patterns, feature stickiness, and lifecycle trends that aggregate metrics obscure.

Implementing the Cohort Analyzer

The cohort analyzer creates cohorts, tracks retention, and generates cohort reports:

// cohort-analyzer.ts - User cohort analysis system
import { getFirestore, collection, query, where, getDocs, Timestamp } from 'firebase/firestore';

interface Cohort {
  id: string;
  name: string;
  description: string;
  criteria: CohortCriteria;
  createdAt: Date;
  userCount: number;
}

interface CohortCriteria {
  type: 'time_based' | 'behavior_based' | 'property_based';
  conditions: Record<string, any>;
}

interface CohortRetention {
  cohortId: string;
  cohortName: string;
  size: number;
  retentionByWeek: number[]; // Percentage retained each week
  retentionByMonth: number[]; // Percentage retained each month
}

export class CohortAnalyzer {
  private firestore = getFirestore();

  /**
   * Create time-based cohort (signup week/month)
   */
  async createTimeCohort(
    name: string,
    startDate: Date,
    endDate: Date
  ): Promise<Cohort> {
    const cohort: Cohort = {
      id: `cohort_${Date.now()}`,
      name,
      description: `Users who signed up between ${startDate.toLocaleDateString()} and ${endDate.toLocaleDateString()}`,
      criteria: {
        type: 'time_based',
        conditions: {
          signupStart: startDate,
          signupEnd: endDate
        }
      },
      createdAt: new Date(),
      userCount: 0
    };

    // Count users in cohort
    cohort.userCount = await this.getCohortSize(cohort);

    return cohort;
  }

  /**
   * Create behavior-based cohort
   */
  async createBehaviorCohort(
    name: string,
    eventName: string,
    minOccurrences: number
  ): Promise<Cohort> {
    const cohort: Cohort = {
      id: `cohort_${Date.now()}`,
      name,
      description: `Users who performed "${eventName}" at least ${minOccurrences} times`,
      criteria: {
        type: 'behavior_based',
        conditions: {
          eventName,
          minOccurrences
        }
      },
      createdAt: new Date(),
      userCount: 0
    };

    cohort.userCount = await this.getCohortSize(cohort);
    return cohort;
  }

  /**
   * Calculate cohort retention
   */
  async calculateRetention(cohort: Cohort, weeks: number = 12): Promise<CohortRetention> {
    const cohortUsers = await this.getCohortUsers(cohort);
    const retentionByWeek: number[] = [];

    for (let week = 0; week < weeks; week++) {
      const activeUsers = await this.getActiveUsersInWeek(cohortUsers, week);
      const retentionRate = (activeUsers / cohort.userCount) * 100;
      retentionByWeek.push(Math.round(retentionRate * 100) / 100);
    }

    // Calculate monthly retention (every 4 weeks)
    const retentionByMonth = retentionByWeek.filter((_, index) => index % 4 === 0);

    return {
      cohortId: cohort.id,
      cohortName: cohort.name,
      size: cohort.userCount,
      retentionByWeek,
      retentionByMonth
    };
  }

  /**
   * Get cohort size
   */
  private async getCohortSize(cohort: Cohort): Promise<number> {
    const users = await this.getCohortUsers(cohort);
    return users.length;
  }

  /**
   * Get users in cohort
   */
  private async getCohortUsers(cohort: Cohort): Promise<string[]> {
    const usersRef = collection(this.firestore, 'userProfiles');
    let q;

    if (cohort.criteria.type === 'time_based') {
      q = query(
        usersRef,
        where('createdAt', '>=', Timestamp.fromDate(cohort.criteria.conditions.signupStart)),
        where('createdAt', '<=', Timestamp.fromDate(cohort.criteria.conditions.signupEnd))
      );
    } else {
      // For behavior-based cohorts, we'd need to query events collection
      return [];
    }

    const snapshot = await getDocs(q);
    return snapshot.docs.map(doc => doc.id);
  }

  /**
   * Get active users in specific week
   */
  private async getActiveUsersInWeek(
    userIds: string[],
    weekNumber: number
  ): Promise<number> {
    // Calculate week start/end dates
    const weekStart = new Date();
    weekStart.setDate(weekStart.getDate() - (weekNumber + 1) * 7);
    const weekEnd = new Date(weekStart);
    weekEnd.setDate(weekEnd.getDate() + 7);

    // Count users with activity in this week
    let activeCount = 0;
    for (const userId of userIds) {
      const hasActivity = await this.userHadActivity(userId, weekStart, weekEnd);
      if (hasActivity) activeCount++;
    }

    return activeCount;
  }

  /**
   * Check if user had activity in date range
   */
  private async userHadActivity(
    userId: string,
    startDate: Date,
    endDate: Date
  ): Promise<boolean> {
    const eventsRef = collection(this.firestore, 'analyticsEvents');
    const q = query(
      eventsRef,
      where('userId', '==', userId),
      where('timestamp', '>=', Timestamp.fromDate(startDate)),
      where('timestamp', '<=', Timestamp.fromDate(endDate))
    );

    const snapshot = await getDocs(q);
    return !snapshot.empty;
  }
}

// Usage example
const cohortAnalyzer = new CohortAnalyzer();

// Create signup cohort for January 2026
const januaryCohort = await cohortAnalyzer.createTimeCohort(
  'January 2026 Signups',
  new Date('2026-01-01'),
  new Date('2026-01-31')
);

// Calculate 12-week retention
const retention = await cohortAnalyzer.calculateRetention(januaryCohort, 12);
console.log('Week 1 retention:', retention.retentionByWeek[0] + '%');
console.log('Week 4 retention:', retention.retentionByWeek[3] + '%');
console.log('Week 12 retention:', retention.retentionByWeek[11] + '%');

This cohort analyzer supports time-based and behavior-based cohorts, calculates weekly and monthly retention, and provides actionable insights into user lifecycle patterns.

Audience Building: Creating Targeted Segments

Audiences combine user properties, behaviors, and cohort membership into actionable segments for personalization, campaigns, and A/B testing. Well-designed audiences enable precise targeting without complex query logic in application code.

Implementing the Audience Builder

The audience builder creates reusable segments with dynamic membership:

// audience-builder.ts - Dynamic audience segmentation
interface AudienceRule {
  property?: string;
  operator: 'equals' | 'not_equals' | 'greater_than' | 'less_than' | 'contains' | 'in';
  value: any;
}

interface Audience {
  id: string;
  name: string;
  description: string;
  rules: AudienceRule[];
  memberCount: number;
  lastUpdated: Date;
}

export class AudienceBuilder {
  private audiences: Map<string, Audience> = new Map();

  /**
   * Create audience with rule-based membership
   */
  createAudience(
    name: string,
    description: string,
    rules: AudienceRule[]
  ): Audience {
    const audience: Audience = {
      id: `audience_${Date.now()}`,
      name,
      description,
      rules,
      memberCount: 0,
      lastUpdated: new Date()
    };

    this.audiences.set(audience.id, audience);
    return audience;
  }

  /**
   * Check if user matches audience
   */
  async userMatchesAudience(
    userId: string,
    audienceId: string,
    userProfile: any
  ): Promise<boolean> {
    const audience = this.audiences.get(audienceId);
    if (!audience) return false;

    // All rules must match (AND logic)
    for (const rule of audience.rules) {
      if (!this.evaluateRule(rule, userProfile)) {
        return false;
      }
    }

    return true;
  }

  /**
   * Evaluate single rule
   */
  private evaluateRule(rule: AudienceRule, userProfile: any): boolean {
    const propertyValue = userProfile.properties[rule.property!]?.value;

    switch (rule.operator) {
      case 'equals':
        return propertyValue === rule.value;
      case 'not_equals':
        return propertyValue !== rule.value;
      case 'greater_than':
        return propertyValue > rule.value;
      case 'less_than':
        return propertyValue < rule.value;
      case 'contains':
        return String(propertyValue).includes(String(rule.value));
      case 'in':
        return Array.isArray(rule.value) && rule.value.includes(propertyValue);
      default:
        return false;
    }
  }

  /**
   * Get predefined audiences
   */
  getPredefinedAudiences(): Audience[] {
    const audiences: Audience[] = [];

    // Power users
    audiences.push(this.createAudience(
      'Power Users',
      'High-engagement users with 50+ tool calls',
      [
        { property: 'total_tool_calls', operator: 'greater_than', value: 50 },
        { property: 'user_tier', operator: 'in', value: ['professional', 'business'] }
      ]
    ));

    // At-risk users
    audiences.push(this.createAudience(
      'At-Risk Users',
      'Paid users with declining engagement',
      [
        { property: 'user_tier', operator: 'not_equals', value: 'free' },
        { property: 'days_since_last_activity', operator: 'greater_than', value: 14 }
      ]
    ));

    // Trial converters
    audiences.push(this.createAudience(
      'Trial Converters',
      'Free users likely to upgrade',
      [
        { property: 'user_tier', operator: 'equals', value: 'free' },
        { property: 'total_tool_calls', operator: 'greater_than', value: 20 },
        { property: 'feature_usage_level', operator: 'equals', value: 'power' }
      ]
    ));

    return audiences;
  }
}

Attribute Enrichment: Enhancing Profiles with External Data

Attribute enrichment combines first-party data (user properties) with third-party data sources (CRM, marketing automation, data warehouses) to create comprehensive user profiles that drive personalization and targeting.

Building the Enrichment Service

// enrichment-service.ts - Third-party data enrichment
interface EnrichmentProvider {
  name: string;
  apiEndpoint: string;
  apiKey: string;
}

export class EnrichmentService {
  private providers: Map<string, EnrichmentProvider> = new Map();

  /**
   * Enrich user with company data (for B2B apps)
   */
  async enrichWithCompanyData(email: string): Promise<Record<string, any>> {
    const domain = email.split('@')[1];

    // Simulated Clearbit-style enrichment
    return {
      company_name: 'Acme Corp',
      company_size: '50-200',
      industry: 'Technology',
      location: 'San Francisco, CA'
    };
  }

  /**
   * Enrich user with behavioral data from CDP
   */
  async enrichWithBehavioralData(userId: string): Promise<Record<string, any>> {
    // Simulated Segment/mParticle enrichment
    return {
      lifetime_value: 450,
      risk_score: 0.15,
      engagement_score: 78,
      predicted_churn_date: '2026-06-15'
    };
  }
}

Personalization Engine: Adapting Experiences by Segment

The personalization engine uses audience membership to adapt ChatGPT app experiences:

// personalization-engine.ts - Experience personalization
export class PersonalizationEngine {
  /**
   * Get personalized welcome message
   */
  getWelcomeMessage(userProfile: any): string {
    const tier = userProfile.properties.user_tier?.value;
    const featureLevel = userProfile.properties.feature_usage_level?.value;

    if (tier === 'free' && featureLevel === 'power') {
      return "You're crushing it! Ready to unlock unlimited tool calls with Pro?";
    } else if (tier === 'professional') {
      return "Welcome back! Your apps are performing great.";
    } else {
      return "Welcome! Let's build your ChatGPT app.";
    }
  }

  /**
   * Get recommended features
   */
  getRecommendedFeatures(userProfile: any): string[] {
    const usedFeatures = userProfile.properties.used_features?.value || [];
    const allFeatures = ['analytics', 'custom_domains', 'api_access', 'team_collaboration'];

    return allFeatures.filter(f => !usedFeatures.includes(f));
  }
}

Conclusion: From Anonymous Users to Rich Segments

User properties and segmentation transform ChatGPT apps from one-size-fits-all experiences into personalized journeys that adapt to each user's context, preferences, and behavior patterns. By enriching profiles with custom properties, tracking dimensions in GA4, analyzing cohorts for retention insights, building dynamic audiences for targeting, and enriching attributes from external sources, you create the foundation for data-driven personalization and engagement.

The production-ready implementations in this guide provide everything you need to build sophisticated segmentation systems: validated property management with GA4 sync, custom dimension tracking with clear configuration docs, cohort analysis with retention calculations, dynamic audience building with rule evaluation, attribute enrichment from third-party sources, and personalization engines that adapt experiences in real-time.

Ready to build ChatGPT apps with advanced user segmentation? Start your free trial with MakeAIHQ and deploy personalized ChatGPT experiences with built-in analytics, audience building, and user property management—no coding required.


Related Resources

Pillar Pages:

  • ChatGPT App Analytics: Complete Implementation Guide
  • ChatGPT App Builder: The Definitive Guide to No-Code Development

Related Cluster Articles:

  • Custom Event Tracking: Events, Parameters & Metadata
  • Funnel Analysis: Conversion Tracking & Drop-off Optimization
  • Cohort Analysis: Retention Tracking & User Lifecycle
  • Analytics Dashboard: Real-time Metrics & Visualization
  • A/B Testing Framework: Experimentation & Optimization

External Resources:


About MakeAIHQ: We're the no-code platform for building ChatGPT apps with advanced analytics, user segmentation, and personalization. From zero to ChatGPT App Store in 48 hours—no coding required.