UNPKG

@warriorteam/redai-zalo-sdk

Version:

Comprehensive TypeScript/JavaScript SDK for Zalo APIs - Official Account, ZNS, Consultation Service, Group Messaging, and Social APIs

1,462 lines (1,191 loc) 42.1 kB
# RedAI Zalo SDK - Tag Management Guide ## Tổng quan **Tag Management** là hệ thống phân loại và quản lý người dùng thông qua các nhãn (tags). Điều này cho phép: - 🏷️ **User Segmentation** - Phân đoạn khách hàng theo nhiều tiêu chí - 📊 **Targeted Marketing** - Gửi tin nhắn có mục tiêu cụ thể - 🎯 **Personalized Content** - Nội dung được cá nhân hóa theo tags - 📈 **Analytics & Insights** - Phân tích hiệu quả theo từng segment - 🔄 **Automation** - Tự động gán tags dựa trên hành vi --- ## TagService Service chính để quản lý tags và tag users. ### Khởi tạo ```typescript import { ZaloSDK } from "@warriorteam/redai-zalo-sdk"; const zalo = new ZaloSDK({ appId: "your-app-id", appSecret: "your-app-secret" }); // Access tag service const tagService = zalo.tag; ``` --- ## Quản lý Tags ### 1. Tạo tag mới ```typescript // Tạo tag mới cho campaign const newTag = await zalo.tag.createTag( accessToken, "BLACK_FRIDAY_2024" ); console.log("Created tag:", newTag.tag_id); console.log("Tag name:", newTag.tag_name); ``` ### 2. Lấy danh sách tags ```typescript // Lấy tất cả tags hiện có const tagList = await zalo.tag.getTagList(accessToken); console.log("Total tags:", tagList.total); tagList.data.forEach(tag => { console.log(`${tag.tag_name}: ${tag.user_count} users`); }); ``` ### 3. Xóa tag ```typescript // Xóa tag không còn sử dụng await zalo.tag.deleteTag(accessToken, "OLD_CAMPAIGN_TAG"); console.log("Tag deleted successfully"); ``` --- ## Gán Tags cho Users ### 1. Tag một user ```typescript // Gán nhiều tags cho một user await zalo.tag.tagUser( accessToken, "user-zalo-id", ["VIP_CUSTOMER", "ELECTRONICS_BUYER", "PREMIUM"] ); console.log("User tagged successfully"); ``` ### 2. Untag user ```typescript // Bỏ tag khỏi user await zalo.tag.untagUser( accessToken, "user-zalo-id", ["OLD_CAMPAIGN", "EXPIRED_SEGMENT"] ); console.log("Tags removed successfully"); ``` ### 3. Lấy tags của user ```typescript // Xem tất cả tags của user const userTags = await zalo.tag.getUserTags( accessToken, "user-zalo-id" ); console.log("User tags:", userTags.map(tag => tag.tag_name)); ``` ### 4. Lấy users theo tag ```typescript // Lấy tất cả users có tag cụ thể const taggedUsers = await zalo.tag.getUsersByTag( accessToken, "VIP_CUSTOMER", { offset: 0, limit: 50 } ); console.log(`Found ${taggedUsers.total} VIP customers`); taggedUsers.data.forEach(user => { console.log(`${user.display_name} - ${user.user_id}`); }); ``` --- ## Tag Strategies & Use Cases ### 1. Customer Lifecycle Tags ```typescript class CustomerLifecycleTagging { constructor(private zalo: ZaloSDK, private accessToken: string) {} async tagUserByLifecycleStage(userId: string, stage: LifecycleStage) { // Remove old lifecycle tags const currentTags = await this.zalo.tag.getUserTags(this.accessToken, userId); const lifecycleTags = currentTags.filter(tag => tag.tag_name.startsWith('LIFECYCLE_') ).map(tag => tag.tag_name); if (lifecycleTags.length > 0) { await this.zalo.tag.untagUser(this.accessToken, userId, lifecycleTags); } // Add new lifecycle tag const newTag = `LIFECYCLE_${stage.toUpperCase()}`; await this.zalo.tag.tagUser(this.accessToken, userId, [newTag]); // Add complementary tags based on stage const complementaryTags = this.getComplementaryTags(stage); if (complementaryTags.length > 0) { await this.zalo.tag.tagUser(this.accessToken, userId, complementaryTags); } } private getComplementaryTags(stage: LifecycleStage): string[] { const tagMappings = { 'new_lead': ['NURTURING_NEEDED', 'WELCOME_SERIES'], 'qualified_lead': ['CONSIDERATION_STAGE', 'SALES_READY'], 'customer': ['ONBOARDING_NEEDED', 'CROSS_SELL_ELIGIBLE'], 'repeat_customer': ['LOYALTY_PROGRAM', 'REFERRAL_CANDIDATE'], 'champion': ['VIP_TREATMENT', 'TESTIMONIAL_CANDIDATE'], 'at_risk': ['RETENTION_CAMPAIGN', 'WIN_BACK_NEEDED'], 'churned': ['RE_ENGAGEMENT', 'FEEDBACK_SURVEY'] }; return tagMappings[stage] || []; } async autoTagBasedOnPurchaseHistory(userId: string) { const userAnalytics = await this.getUserPurchaseAnalytics(userId); const tagsToAdd = []; // Value-based tags if (userAnalytics.totalSpent > 50000000) { tagsToAdd.push('HIGH_VALUE_CUSTOMER'); } else if (userAnalytics.totalSpent > 10000000) { tagsToAdd.push('MEDIUM_VALUE_CUSTOMER'); } // Frequency-based tags if (userAnalytics.purchaseCount > 10) { tagsToAdd.push('FREQUENT_BUYER'); } // Recency-based tags const daysSinceLastPurchase = userAnalytics.daysSinceLastPurchase; if (daysSinceLastPurchase > 90) { tagsToAdd.push('INACTIVE_BUYER'); } else if (daysSinceLastPurchase <= 30) { tagsToAdd.push('ACTIVE_BUYER'); } // Category preferences const topCategory = userAnalytics.topPurchaseCategory; if (topCategory) { tagsToAdd.push(`CATEGORY_${topCategory.toUpperCase()}`); } if (tagsToAdd.length > 0) { await this.zalo.tag.tagUser(this.accessToken, userId, tagsToAdd); } } } ``` ### 2. Behavioral Tags ```typescript class BehavioralTagging { constructor(private zalo: ZaloSDK, private accessToken: string) {} async tagUsersByEngagement() { const users = await this.getAllActiveUsers(); for (const user of users) { const engagementLevel = await this.calculateEngagementLevel(user.user_id); await this.applyEngagementTags(user.user_id, engagementLevel); } } private async calculateEngagementLevel(userId: string): Promise<EngagementLevel> { const interactions = await this.zalo.userManagement.getUserInteractions( this.accessToken, userId, { from_time: Date.now() - (30 * 24 * 60 * 60 * 1000), // 30 days limit: 1000 } ); const messagesSent = interactions.filter(i => i.from_user).length; const messagesReceived = interactions.filter(i => !i.from_user).length; const avgResponseTime = this.calculateAverageResponseTime(interactions); const score = this.calculateEngagementScore( messagesSent, messagesReceived, avgResponseTime ); if (score > 80) return 'very_high'; if (score > 60) return 'high'; if (score > 40) return 'medium'; if (score > 20) return 'low'; return 'very_low'; } private async applyEngagementTags(userId: string, level: EngagementLevel) { // Remove old engagement tags const engagementTags = [ 'ENGAGEMENT_VERY_HIGH', 'ENGAGEMENT_HIGH', 'ENGAGEMENT_MEDIUM', 'ENGAGEMENT_LOW', 'ENGAGEMENT_VERY_LOW' ]; const currentTags = await this.zalo.tag.getUserTags(this.accessToken, userId); const oldEngagementTags = currentTags .filter(tag => engagementTags.includes(tag.tag_name)) .map(tag => tag.tag_name); if (oldEngagementTags.length > 0) { await this.zalo.tag.untagUser(this.accessToken, userId, oldEngagementTags); } // Add new engagement tag const newTag = `ENGAGEMENT_${level.toUpperCase()}`; await this.zalo.tag.tagUser(this.accessToken, userId, [newTag]); // Add action-based tags const actionTags = this.getActionBasedTags(level); if (actionTags.length > 0) { await this.zalo.tag.tagUser(this.accessToken, userId, actionTags); } } private getActionBasedTags(level: EngagementLevel): string[] { const actionMappings = { 'very_high': ['BRAND_ADVOCATE', 'REFERRAL_READY', 'BETA_TESTER'], 'high': ['LOYAL_CUSTOMER', 'UPSELL_READY'], 'medium': ['NURTURING_NEEDED', 'ENGAGEMENT_BOOST'], 'low': ['RE_ENGAGEMENT_NEEDED'], 'very_low': ['CHURN_RISK', 'WIN_BACK_CAMPAIGN'] }; return actionMappings[level] || []; } } ``` ### 3. Campaign Tags ```typescript class CampaignTagging { constructor(private zalo: ZaloSDK, private accessToken: string) {} // Tạo campaign tags tự động async createCampaignTags(campaign: Campaign) { const campaignTags = [ `CAMPAIGN_${campaign.id.toUpperCase()}`, `CAMPAIGN_${campaign.type.toUpperCase()}`, `CAMPAIGN_${campaign.startDate.getFullYear()}_${campaign.startDate.getMonth() + 1}` ]; for (const tagName of campaignTags) { try { await this.zalo.tag.createTag(this.accessToken, tagName); console.log(`Created campaign tag: ${tagName}`); } catch (error) { if (error.code !== -224) { // Tag already exists console.error(`Failed to create tag ${tagName}:`, error); } } } return campaignTags; } // Tag users dựa trên response với campaign async tagCampaignResponders(campaignId: string) { const campaignMessages = await this.getCampaignMessages(campaignId); const responders = new Set<string>(); const nonResponders = new Set<string>(); // Identify responders vs non-responders for (const message of campaignMessages) { const hasResponse = await this.checkUserResponse(message.user_id, message.sent_time); if (hasResponse) { responders.add(message.user_id); } else { nonResponders.add(message.user_id); } } // Tag responders const responderTag = `CAMPAIGN_${campaignId}_RESPONDER`; for (const userId of responders) { await this.zalo.tag.tagUser(this.accessToken, userId, [responderTag]); } // Tag non-responders for follow-up const nonResponderTag = `CAMPAIGN_${campaignId}_NON_RESPONDER`; for (const userId of nonResponders) { await this.zalo.tag.tagUser(this.accessToken, userId, [nonResponderTag]); } return { responders: responders.size, nonResponders: nonResponders.size, responseRate: (responders.size / (responders.size + nonResponders.size) * 100).toFixed(2) + '%' }; } // Auto cleanup expired campaign tags async cleanupExpiredCampaignTags() { const tagList = await this.zalo.tag.getTagList(this.accessToken); const campaignTags = tagList.data.filter(tag => tag.tag_name.startsWith('CAMPAIGN_') ); for (const tag of campaignTags) { const isExpired = await this.isCampaignExpired(tag.tag_name); if (isExpired && tag.user_count === 0) { await this.zalo.tag.deleteTag(this.accessToken, tag.tag_name); console.log(`Deleted expired campaign tag: ${tag.tag_name}`); } else if (isExpired) { // Move users to archived tag const archivedTag = `ARCHIVED_${tag.tag_name}`; await this.zalo.tag.createTag(this.accessToken, archivedTag); const users = await this.zalo.tag.getUsersByTag(this.accessToken, tag.tag_name); for (const user of users.data) { await this.zalo.tag.tagUser(this.accessToken, user.user_id, [archivedTag]); await this.zalo.tag.untagUser(this.accessToken, user.user_id, [tag.tag_name]); } } } } } ``` --- ## Advanced Tag Operations ### 1. Bulk Tag Operations ```typescript class BulkTagOperations { constructor(private zalo: ZaloSDK, private accessToken: string) {} async bulkTagUsers( userIds: string[], tagsToAdd: string[], tagsToRemove: string[] = [] ): Promise<BulkTagResult> { const results = { successful: 0, failed: 0, errors: [] as Array<{userId: string, error: string}> }; const batchSize = 5; // Process 5 users at a time to avoid rate limits for (let i = 0; i < userIds.length; i += batchSize) { const batch = userIds.slice(i, i + batchSize); const promises = batch.map(async (userId) => { try { // Remove tags first if (tagsToRemove.length > 0) { await this.zalo.tag.untagUser(this.accessToken, userId, tagsToRemove); } // Add new tags if (tagsToAdd.length > 0) { await this.zalo.tag.tagUser(this.accessToken, userId, tagsToAdd); } return { userId, success: true }; } catch (error) { return { userId, success: false, error: error.message }; } }); const batchResults = await Promise.all(promises); batchResults.forEach(result => { if (result.success) { results.successful++; } else { results.failed++; results.errors.push({ userId: result.userId, error: result.error || 'Unknown error' }); } }); // Delay between batches if (i + batchSize < userIds.length) { await this.delay(1000); } } return results; } async conditionalBulkTagging( condition: TagCondition, tagsToApply: string[] ): Promise<BulkTagResult> { const matchingUsers = await this.findUsersMatchingCondition(condition); console.log(`Found ${matchingUsers.length} users matching condition`); return this.bulkTagUsers( matchingUsers.map(u => u.user_id), tagsToApply ); } private async findUsersMatchingCondition(condition: TagCondition): Promise<UserProfile[]> { const allUsers = await this.getAllUsers(); const matchingUsers = []; for (const user of allUsers) { const matches = await this.evaluateCondition(user, condition); if (matches) { matchingUsers.push(user); } } return matchingUsers; } private async evaluateCondition(user: UserProfile, condition: TagCondition): Promise<boolean> { switch (condition.type) { case 'has_tags': const userTags = await this.zalo.tag.getUserTags(this.accessToken, user.user_id); const userTagNames = userTags.map(tag => tag.tag_name); return condition.tags.every(tag => userTagNames.includes(tag)); case 'missing_tags': const currentTags = await this.zalo.tag.getUserTags(this.accessToken, user.user_id); const currentTagNames = currentTags.map(tag => tag.tag_name); return condition.tags.some(tag => !currentTagNames.includes(tag)); case 'purchase_amount': const analytics = await this.getUserAnalytics(user.user_id); return this.evaluateNumericCondition(analytics.totalSpent, condition.operator, condition.value); case 'last_interaction': const lastInteraction = await this.getLastInteractionDays(user.user_id); return this.evaluateNumericCondition(lastInteraction, condition.operator, condition.value); default: return false; } } private delay(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } } ``` ### 2. Smart Tag Suggestions ```typescript class SmartTagSuggestions { constructor(private zalo: ZaloSDK, private accessToken: string) {} async suggestTagsForUser(userId: string): Promise<TagSuggestion[]> { const userProfile = await this.zalo.userManagement.getUserProfile( this.accessToken, userId ); const userInteractions = await this.zalo.userManagement.getUserInteractions( this.accessToken, userId, { limit: 500 } ); const suggestions = []; // Behavioral suggestions const behaviorSuggestions = await this.suggestBehavioralTags(userInteractions); suggestions.push(...behaviorSuggestions); // Value-based suggestions const valueSuggestions = await this.suggestValueBasedTags(userInteractions); suggestions.push(...valueSuggestions); // Interest-based suggestions const interestSuggestions = await this.suggestInterestBasedTags(userInteractions); suggestions.push(...interestSuggestions); // Lifecycle suggestions const lifecycleSuggestions = await this.suggestLifecycleTags(userProfile, userInteractions); suggestions.push(...lifecycleSuggestions); return suggestions.sort((a, b) => b.confidence - a.confidence).slice(0, 10); } private async suggestBehavioralTags(interactions: any[]): Promise<TagSuggestion[]> { const suggestions = []; // Analyze response patterns const responseTime = this.calculateAverageResponseTime(interactions); if (responseTime < 300000) { // 5 minutes suggestions.push({ tag: 'QUICK_RESPONDER', confidence: 0.9, reason: 'User responds quickly to messages' }); } // Analyze interaction frequency const monthlyInteractions = this.countInteractionsInPeriod(interactions, 30); if (monthlyInteractions > 20) { suggestions.push({ tag: 'HIGHLY_ENGAGED', confidence: 0.8, reason: 'High interaction frequency' }); } // Analyze message types const questionCount = interactions.filter(i => i.message && i.message.includes('?') ).length; if (questionCount > interactions.length * 0.3) { suggestions.push({ tag: 'INQUISITIVE', confidence: 0.7, reason: 'Asks many questions' }); } return suggestions; } private async suggestValueBasedTags(interactions: any[]): Promise<TagSuggestion[]> { const suggestions = []; const purchases = interactions.filter(i => i.type === 'purchase'); if (purchases.length === 0) return suggestions; const totalValue = purchases.reduce((sum, p) => sum + (p.value || 0), 0); const avgOrderValue = totalValue / purchases.length; if (totalValue > 50000000) { suggestions.push({ tag: 'HIGH_VALUE_CUSTOMER', confidence: 0.95, reason: `Total purchases: ${this.formatCurrency(totalValue)}` }); } else if (totalValue > 10000000) { suggestions.push({ tag: 'MEDIUM_VALUE_CUSTOMER', confidence: 0.85, reason: `Total purchases: ${this.formatCurrency(totalValue)}` }); } if (avgOrderValue > 5000000) { suggestions.push({ tag: 'PREMIUM_BUYER', confidence: 0.8, reason: `Average order: ${this.formatCurrency(avgOrderValue)}` }); } return suggestions; } async autoApplySuggestedTags(userId: string, minConfidence: number = 0.8) { const suggestions = await this.suggestTagsForUser(userId); const highConfidenceTags = suggestions .filter(s => s.confidence >= minConfidence) .map(s => s.tag); if (highConfidenceTags.length > 0) { await this.zalo.tag.tagUser(this.accessToken, userId, highConfidenceTags); console.log(`Auto-applied ${highConfidenceTags.length} tags to user ${userId}:`); highConfidenceTags.forEach(tag => console.log(` - ${tag}`)); } } } ``` --- ## Tag Analytics & Insights ### 1. Tag Performance Analytics ```typescript class TagAnalytics { constructor(private zalo: ZaloSDK, private accessToken: string) {} async analyzeTagPerformance(tagName: string, period: number = 30): Promise<TagAnalytics> { const taggedUsers = await this.zalo.tag.getUsersByTag( this.accessToken, tagName, { limit: 1000 } ); const analytics = { totalUsers: taggedUsers.total, activeUsers: 0, totalInteractions: 0, totalRevenue: 0, avgEngagement: 0, conversionRate: 0, retentionRate: 0 }; const cutoffTime = Date.now() - (period * 24 * 60 * 60 * 1000); let totalEngagement = 0; let conversions = 0; for (const user of taggedUsers.data) { const userAnalytics = await this.zalo.userManagement.getUserAnalytics( this.accessToken, user.user_id ); const interactions = await this.zalo.userManagement.getUserInteractions( this.accessToken, user.user_id, { from_time: cutoffTime, limit: 1000 } ); if (interactions.length > 0) { analytics.activeUsers++; analytics.totalInteractions += interactions.length; const engagement = this.calculateEngagementScore(interactions); totalEngagement += engagement; // Check for conversions (purchases) in the period const purchases = interactions.filter(i => i.type === 'purchase'); if (purchases.length > 0) { conversions++; const revenue = purchases.reduce((sum, p) => sum + (p.value || 0), 0); analytics.totalRevenue += revenue; } } } analytics.avgEngagement = analytics.activeUsers > 0 ? totalEngagement / analytics.activeUsers : 0; analytics.conversionRate = analytics.activeUsers > 0 ? conversions / analytics.activeUsers : 0; analytics.retentionRate = analytics.totalUsers > 0 ? analytics.activeUsers / analytics.totalUsers : 0; return analytics; } async compareTagPerformance(tags: string[]): Promise<TagComparisonReport> { const comparisons = []; for (const tag of tags) { const performance = await this.analyzeTagPerformance(tag); comparisons.push({ tag, ...performance }); } // Sort by total revenue descending comparisons.sort((a, b) => b.totalRevenue - a.totalRevenue); return { period: '30 days', comparisons, insights: this.generateComparisonInsights(comparisons) }; } private generateComparisonInsights(comparisons: any[]): string[] { const insights = []; // Find best performing tag by revenue if (comparisons.length > 0) { const topTag = comparisons[0]; insights.push(`${topTag.tag} is the highest revenue generating segment with ${this.formatCurrency(topTag.totalRevenue)}`); } // Find most engaged segment const mostEngaged = comparisons.reduce((prev, current) => current.avgEngagement > prev.avgEngagement ? current : prev ); insights.push(`${mostEngaged.tag} has the highest engagement with ${mostEngaged.avgEngagement.toFixed(2)} score`); // Find best conversion rate const bestConversion = comparisons.reduce((prev, current) => current.conversionRate > prev.conversionRate ? current : prev ); insights.push(`${bestConversion.tag} has the best conversion rate at ${(bestConversion.conversionRate * 100).toFixed(2)}%`); return insights; } async generateTagInsights(): Promise<TagInsights> { const tagList = await this.zalo.tag.getTagList(this.accessToken); const insights = { totalTags: tagList.total, mostPopularTags: [], underutilizedTags: [], orphanedTags: [], recommendedCleanup: [] }; // Find most popular tags const sortedByUsers = tagList.data.sort((a, b) => b.user_count - a.user_count); insights.mostPopularTags = sortedByUsers.slice(0, 10); // Find underutilized tags insights.underutilizedTags = tagList.data.filter(tag => tag.user_count > 0 && tag.user_count < 5 ); // Find orphaned tags (no users) insights.orphanedTags = tagList.data.filter(tag => tag.user_count === 0); // Recommend cleanup insights.recommendedCleanup = [ ...insights.orphanedTags.map(tag => ({ tag: tag.tag_name, reason: 'No users tagged', action: 'Delete' })), ...insights.underutilizedTags .filter(tag => tag.tag_name.includes('CAMPAIGN_')) .map(tag => ({ tag: tag.tag_name, reason: 'Old campaign with few users', action: 'Archive or delete' })) ]; return insights; } } ``` ### 2. Tag-based Campaign Analysis ```typescript class TagCampaignAnalytics { constructor(private zalo: ZaloSDK, private accessToken: string) {} async analyzeCampaignByTags( campaignId: string, targetTags: string[] ): Promise<CampaignTagAnalytics> { const results = new Map<string, TagCampaignMetrics>(); for (const tag of targetTags) { const taggedUsers = await this.zalo.tag.getUsersByTag( this.accessToken, tag, { limit: 1000 } ); const campaignMetrics = await this.analyzeCampaignForSegment( campaignId, taggedUsers.data ); results.set(tag, { segmentSize: taggedUsers.total, ...campaignMetrics }); } return { campaignId, tagPerformance: Array.from(results.entries()), overallInsights: this.generateCampaignInsights(results) }; } private async analyzeCampaignForSegment( campaignId: string, users: UserProfile[] ): Promise<CampaignMetrics> { let messagesSent = 0; let messagesRead = 0; let responses = 0; let conversions = 0; let totalRevenue = 0; for (const user of users) { const campaignInteractions = await this.getCampaignInteractionsForUser( campaignId, user.user_id ); if (campaignInteractions.length > 0) { messagesSent++; // Check if messages were read const readMessages = campaignInteractions.filter(i => i.read); if (readMessages.length > 0) { messagesRead++; } // Check for responses const userResponses = campaignInteractions.filter(i => i.from_user); if (userResponses.length > 0) { responses++; } // Check for conversions const purchases = campaignInteractions.filter(i => i.type === 'purchase'); if (purchases.length > 0) { conversions++; totalRevenue += purchases.reduce((sum, p) => sum + (p.value || 0), 0); } } } return { messagesSent, deliveryRate: messagesSent / users.length, readRate: messagesRead / messagesSent, responseRate: responses / messagesSent, conversionRate: conversions / messagesSent, totalRevenue, revenuePerUser: totalRevenue / users.length }; } async optimizeTagsForCampaign( historicalCampaignData: CampaignHistory[] ): Promise<TagOptimizationRecommendations> { const tagPerformanceMap = new Map<string, TagPerformanceHistory>(); // Analyze historical performance for (const campaign of historicalCampaignData) { const tagAnalytics = await this.analyzeCampaignByTags( campaign.id, campaign.targetTags ); for (const [tag, metrics] of tagAnalytics.tagPerformance) { if (!tagPerformanceMap.has(tag)) { tagPerformanceMap.set(tag, { campaigns: [], avgConversionRate: 0, avgRevenuePerUser: 0, consistency: 0 }); } const tagHistory = tagPerformanceMap.get(tag)!; tagHistory.campaigns.push(metrics); } } // Calculate averages and consistency for (const [tag, history] of tagPerformanceMap) { const campaigns = history.campaigns; history.avgConversionRate = campaigns.reduce((sum, c) => sum + c.conversionRate, 0) / campaigns.length; history.avgRevenuePerUser = campaigns.reduce((sum, c) => sum + c.revenuePerUser, 0) / campaigns.length; history.consistency = 1 - this.calculateVariance(campaigns.map(c => c.conversionRate)); } // Generate recommendations const sortedTags = Array.from(tagPerformanceMap.entries()) .sort(([,a], [,b]) => b.avgRevenuePerUser - a.avgRevenuePerUser); return { topPerformingTags: sortedTags.slice(0, 5).map(([tag, perf]) => ({ tag, avgConversionRate: perf.avgConversionRate, avgRevenuePerUser: perf.avgRevenuePerUser, recommendation: 'Prioritize for high-value campaigns' })), consistentTags: sortedTags .filter(([,perf]) => perf.consistency > 0.8) .slice(0, 5) .map(([tag, perf]) => ({ tag, consistency: perf.consistency, recommendation: 'Reliable for predictable results' })), underperformingTags: sortedTags .slice(-5) .map(([tag, perf]) => ({ tag, avgConversionRate: perf.avgConversionRate, recommendation: 'Consider re-segmenting or exclusion' })) }; } } ``` --- ## Automated Tag Management ### 1. Auto-tagging Rules Engine ```typescript class AutoTaggingEngine { private rules: TaggingRule[] = []; constructor(private zalo: ZaloSDK, private accessToken: string) { this.initializeDefaultRules(); } addRule(rule: TaggingRule): void { this.rules.push({ ...rule, id: this.generateRuleId(), createdAt: Date.now() }); } async processUser(userId: string): Promise<TaggingResult> { const userProfile = await this.zalo.userManagement.getUserProfile( this.accessToken, userId ); const userAnalytics = await this.zalo.userManagement.getUserAnalytics( this.accessToken, userId ); const appliedRules = []; const tagsAdded = []; const tagsRemoved = []; for (const rule of this.rules) { if (await this.evaluateRule(rule, userProfile, userAnalytics)) { const result = await this.applyRule(rule, userId); appliedRules.push(rule.id); tagsAdded.push(...result.tagsAdded); tagsRemoved.push(...result.tagsRemoved); } } return { userId, appliedRules, tagsAdded, tagsRemoved, timestamp: Date.now() }; } private async evaluateRule( rule: TaggingRule, userProfile: UserProfile, userAnalytics: any ): Promise<boolean> { for (const condition of rule.conditions) { const result = await this.evaluateCondition(condition, userProfile, userAnalytics); if (rule.operator === 'AND' && !result) { return false; } else if (rule.operator === 'OR' && result) { return true; } } return rule.operator === 'AND'; } private async evaluateCondition( condition: TagCondition, userProfile: UserProfile, userAnalytics: any ): Promise<boolean> { switch (condition.type) { case 'total_spent': return this.compareValues( userAnalytics.total_spent || 0, condition.operator, condition.value ); case 'days_since_last_purchase': return this.compareValues( userAnalytics.days_since_last_purchase || 999, condition.operator, condition.value ); case 'engagement_score': return this.compareValues( userAnalytics.engagement_score || 0, condition.operator, condition.value ); case 'has_tag': const userTags = await this.zalo.tag.getUserTags(this.accessToken, userProfile.user_id); const tagNames = userTags.map(tag => tag.tag_name); return tagNames.includes(condition.value); case 'purchase_category': const topCategory = userAnalytics.top_purchase_category; return topCategory === condition.value; case 'interaction_frequency': return this.compareValues( userAnalytics.monthly_interactions || 0, condition.operator, condition.value ); default: return false; } } private async applyRule(rule: TaggingRule, userId: string): Promise<RuleApplication> { const tagsAdded = []; const tagsRemoved = []; // Add tags if (rule.actions.addTags && rule.actions.addTags.length > 0) { await this.zalo.tag.tagUser(this.accessToken, userId, rule.actions.addTags); tagsAdded.push(...rule.actions.addTags); } // Remove tags if (rule.actions.removeTags && rule.actions.removeTags.length > 0) { await this.zalo.tag.untagUser(this.accessToken, userId, rule.actions.removeTags); tagsRemoved.push(...rule.actions.removeTags); } return { tagsAdded, tagsRemoved }; } private initializeDefaultRules(): void { // VIP Customer Rule this.addRule({ name: 'VIP Customer Auto-Tag', conditions: [ { type: 'total_spent', operator: 'gte', value: 50000000 }, { type: 'engagement_score', operator: 'gte', value: 70 } ], operator: 'AND', actions: { addTags: ['VIP_CUSTOMER', 'HIGH_VALUE'], removeTags: ['STANDARD_CUSTOMER'] }, active: true }); // Churn Risk Rule this.addRule({ name: 'Churn Risk Detection', conditions: [ { type: 'days_since_last_purchase', operator: 'gte', value: 90 }, { type: 'engagement_score', operator: 'lte', value: 30 } ], operator: 'AND', actions: { addTags: ['CHURN_RISK', 'RE_ENGAGEMENT_NEEDED'], removeTags: ['ACTIVE_CUSTOMER'] }, active: true }); // Frequent Buyer Rule this.addRule({ name: 'Frequent Buyer', conditions: [ { type: 'interaction_frequency', operator: 'gte', value: 15 }, { type: 'days_since_last_purchase', operator: 'lte', value: 30 } ], operator: 'AND', actions: { addTags: ['FREQUENT_BUYER', 'LOYALTY_PROGRAM_ELIGIBLE'] }, active: true }); } // Batch process all users async processBatch(userIds?: string[]): Promise<BatchTaggingResult> { const usersToProcess = userIds || await this.getAllActiveUserIds(); const results = []; const errors = []; console.log(`Processing ${usersToProcess.length} users with ${this.rules.length} rules`); for (const userId of usersToProcess) { try { const result = await this.processUser(userId); results.push(result); } catch (error) { errors.push({ userId, error: error.message }); } } return { totalProcessed: usersToProcess.length, successful: results.length, failed: errors.length, results, errors }; } } ``` ### 2. Scheduled Tag Maintenance ```typescript class TagMaintenanceScheduler { constructor(private zalo: ZaloSDK, private accessToken: string) {} async runDailyMaintenance(): Promise<MaintenanceReport> { console.log('Starting daily tag maintenance...'); const tasks = [ this.cleanupOrphanedTags(), this.archiveExpiredCampaignTags(), this.updateLifecycleTags(), this.refreshEngagementTags(), this.validateTagConsistency() ]; const results = await Promise.allSettled(tasks); return { timestamp: Date.now(), tasks: results.map((result, index) => ({ task: ['cleanup', 'archive', 'lifecycle', 'engagement', 'validate'][index], status: result.status, result: result.status === 'fulfilled' ? result.value : result.reason })) }; } private async cleanupOrphanedTags(): Promise<CleanupResult> { const tagList = await this.zalo.tag.getTagList(this.accessToken); const orphanedTags = tagList.data.filter(tag => tag.user_count === 0); let deleted = 0; for (const tag of orphanedTags) { try { await this.zalo.tag.deleteTag(this.accessToken, tag.tag_name); deleted++; } catch (error) { console.error(`Failed to delete orphaned tag ${tag.tag_name}:`, error); } } return { deleted, total: orphanedTags.length }; } private async archiveExpiredCampaignTags(): Promise<ArchiveResult> { const tagList = await this.zalo.tag.getTagList(this.accessToken); const campaignTags = tagList.data.filter(tag => tag.tag_name.startsWith('CAMPAIGN_') && this.isCampaignExpired(tag.tag_name) ); let archived = 0; for (const tag of campaignTags) { try { const archivedTagName = `ARCHIVED_${tag.tag_name}`; await this.zalo.tag.createTag(this.accessToken, archivedTagName); // Move users to archived tag const users = await this.zalo.tag.getUsersByTag(this.accessToken, tag.tag_name); for (const user of users.data) { await this.zalo.tag.tagUser(this.accessToken, user.user_id, [archivedTagName]); await this.zalo.tag.untagUser(this.accessToken, user.user_id, [tag.tag_name]); } await this.zalo.tag.deleteTag(this.accessToken, tag.tag_name); archived++; } catch (error) { console.error(`Failed to archive campaign tag ${tag.tag_name}:`, error); } } return { archived, total: campaignTags.length }; } private async updateLifecycleTags(): Promise<UpdateResult> { const lifecycleTagging = new CustomerLifecycleTagging(this.zalo, this.accessToken); const allUsers = await this.getAllActiveUsers(); let updated = 0; for (const user of allUsers) { try { const currentStage = await this.determineLifecycleStage(user.user_id); await lifecycleTagging.tagUserByLifecycleStage(user.user_id, currentStage); updated++; } catch (error) { console.error(`Failed to update lifecycle tags for user ${user.user_id}:`, error); } } return { updated, total: allUsers.length }; } private isCampaignExpired(tagName: string): boolean { // Extract date from campaign tag name const dateMatch = tagName.match(/(\d{4})_(\d{1,2})/); if (!dateMatch) return false; const [, year, month] = dateMatch; const campaignDate = new Date(parseInt(year), parseInt(month) - 1); const threeMonthsAgo = new Date(); threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3); return campaignDate < threeMonthsAgo; } } ``` --- ## Best Practices ### 1. Tag Naming Conventions ```typescript // ✅ Good tag naming patterns const tagNamingPatterns = { // Category_SubCategory_Qualifier customer_segments: [ 'CUSTOMER_VIP_DIAMOND', 'CUSTOMER_VIP_GOLD', 'CUSTOMER_REGULAR_ACTIVE', 'CUSTOMER_REGULAR_INACTIVE' ], // Action_Target_Timeframe campaigns: [ 'CAMPAIGN_BLACKFRIDAY_2024', 'CAMPAIGN_NEWPRODUCT_Q1', 'CAMPAIGN_RETENTION_MONTHLY' ], // Behavior_Level_Context engagement: [ 'ENGAGEMENT_HIGH_RECENT', 'ENGAGEMENT_LOW_ATRISK', 'ENGAGEMENT_NONE_CHURNED' ], // Lifecycle_Stage_Status lifecycle: [ 'LIFECYCLE_PROSPECT_NEW', 'LIFECYCLE_CUSTOMER_ACTIVE', 'LIFECYCLE_CUSTOMER_REPEAT' ] }; // ❌ Avoid these patterns const badTagNames = [ 'tag1', 'customers', 'important', 'temp', 'test' ]; ``` ### 2. Tag Architecture Best Practices ```typescript class TagArchitectureBestPractices { // Use hierarchical tagging static getTagHierarchy(): TagHierarchy { return { 'CUSTOMER': { 'CUSTOMER_VALUE': ['HIGH', 'MEDIUM', 'LOW'], 'CUSTOMER_STATUS': ['ACTIVE', 'INACTIVE', 'CHURNED'], 'CUSTOMER_TYPE': ['B2B', 'B2C', 'ENTERPRISE'] }, 'ENGAGEMENT': { 'ENGAGEMENT_LEVEL': ['VERY_HIGH', 'HIGH', 'MEDIUM', 'LOW'], 'ENGAGEMENT_CHANNEL': ['ZALO', 'EMAIL', 'SMS', 'PHONE'] }, 'LIFECYCLE': { 'LIFECYCLE_STAGE': ['AWARENESS', 'CONSIDERATION', 'PURCHASE', 'RETENTION', 'ADVOCACY'] } }; } // Implement mutual exclusivity where appropriate static getMutuallyExclusiveTags(): MutuallyExclusiveGroups { return [ ['CUSTOMER_VALUE_HIGH', 'CUSTOMER_VALUE_MEDIUM', 'CUSTOMER_VALUE_LOW'], ['LIFECYCLE_AWARENESS', 'LIFECYCLE_CONSIDERATION', 'LIFECYCLE_PURCHASE', 'LIFECYCLE_RETENTION'], ['ENGAGEMENT_VERY_HIGH', 'ENGAGEMENT_HIGH', 'ENGAGEMENT_MEDIUM', 'ENGAGEMENT_LOW'] ]; } // Tag governance rules static getTagGovernanceRules(): TagGovernanceRules { return { maxTagsPerUser: 25, requireApprovalForTags: ['VIP_*', 'ENTERPRISE_*'], autoExpirationTags: ['CAMPAIGN_*', 'PROMOTION_*'], protectedTags: ['SYSTEM_*', 'ADMIN_*'], requiredTags: ['LIFECYCLE_*', 'CUSTOMER_VALUE_*'] }; } } ``` --- ## Testing Tag Management ### 1. Unit Tests ```typescript // tag-management.test.ts import { ZaloSDK } from '@warriorteam/redai-zalo-sdk'; describe('Tag Management', () => { const zalo = new ZaloSDK({ appId: 'test_app_id', appSecret: 'test_app_secret' }); it('should create and delete tags', async () => { const mockTagResponse = { error: 0, message: 'Success', data: { tag_id: 'test_tag_id', tag_name: 'TEST_TAG' } }; jest.spyOn(zalo.tag, 'createTag').mockResolvedValue(mockTagResponse); jest.spyOn(zalo.tag, 'deleteTag').mockResolvedValue({ error: 0 }); // Test tag creation const result = await zalo.tag.createTag('test_token', 'TEST_TAG'); expect(result.tag_name).toBe('TEST_TAG'); // Test tag deletion await expect(zalo.tag.deleteTag('test_token', 'TEST_TAG')).resolves.not.toThrow(); }); it('should tag and untag users', async () => { jest.spyOn(zalo.tag, 'tagUser').mockResolvedValue({ error: 0 }); jest.spyOn(zalo.tag, 'untagUser').mockResolvedValue({ error: 0 }); await expect(zalo.tag.tagUser('test_token', 'user123', ['TAG1', 'TAG2'])).resolves.not.toThrow(); await expect(zalo.tag.untagUser('test_token', 'user123', ['TAG1'])).resolves.not.toThrow(); }); }); ``` --- ## Troubleshooting ### Common Issues **Q: "Tag already exists" error** ``` A: Kiểm tra danh sách tags trước khi tạo mới. Sử dụng try-catch để handle lỗi này gracefully. ``` **Q: User không được tag thành công** ``` A: Kiểm tra: - User ID có hợp lệ không - User có còn follow OA không - Tag name có đúng format không - Có đủ quyền để tag user không ``` **Q: Bulk tagging bị rate limited** ``` A: Implement batch processing với delays: - Process 5-10 users per batch - Add 1-2 second delays between batches - Implement exponential backoff for retries ``` --- ## Next Steps Sau khi nắm vững Tag Management: 1. **[Video Upload](./VIDEO_UPLOAD.md)** - Upload và manage media content 2. **[Error Handling](./ERROR_HANDLING.md)** - Comprehensive error handling 3. **[Group Management](./GROUP_MANAGEMENT.md)** - Zalo group operations 4. **[Webhook Events](./WEBHOOK_EVENTS.md)** - Real-time event processing Tham khảo **[API Reference](./API_REFERENCE.md)** để biết chi tiết về tất cả tag management methods.