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:
- Google Analytics 4: Custom Dimensions Guide
- Amplitude: Cohort Analysis Best Practices
- Segment: Audience Segmentation Strategies
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.