UNPKG

@warriorteam/redai-zalo-sdk

Version:

Comprehensive TypeScript/JavaScript SDK for Zalo APIs - Official Account v3.0, ZNS with Full Type Safety, Consultation Service, Broadcast Service, Group Messaging with List APIs, Social APIs, Enhanced Article Management, Promotion Service v3.0 with Multip

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.