@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
Markdown
# 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.