analytics.js•5.24 kB
/**
* Analytics model
* Stores analytics data for Facebook ads
*/
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const analyticsSchema = new Schema({
// Entity ID (campaign, ad set, or ad)
entityId: {
type: String,
required: true,
index: true
},
// Entity type
entityType: {
type: String,
enum: ['campaign', 'adset', 'ad'],
required: true,
index: true
},
// User who owns this entity
userId: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true,
index: true
},
// Date of analytics data
date: {
type: Date,
required: true,
index: true
},
// Performance metrics
impressions: {
type: Number,
default: 0
},
reach: {
type: Number,
default: 0
},
clicks: {
type: Number,
default: 0
},
ctr: {
type: Number,
default: 0
},
cpc: {
type: Number,
default: 0
},
cpm: {
type: Number,
default: 0
},
spend: {
type: Number,
default: 0
},
frequency: {
type: Number,
default: 0
},
// Conversion metrics
conversions: {
type: Number,
default: 0
},
conversionRate: {
type: Number,
default: 0
},
costPerConversion: {
type: Number,
default: 0
},
roas: {
type: Number,
default: 0
},
// Engagement metrics
engagement: {
type: Schema.Types.Mixed,
default: {
postEngagement: 0,
pageLikes: 0,
postComments: 0,
postShares: 0,
linkClicks: 0,
videoViews: 0,
videoWatchTime: 0
}
},
// Demographic breakdown
demographics: {
type: Schema.Types.Mixed
},
// Placement breakdown
placements: {
type: Schema.Types.Mixed
},
// Device breakdown
devices: {
type: Schema.Types.Mixed
},
// Region breakdown
regions: {
type: Schema.Types.Mixed
},
// Raw data from Facebook
rawData: {
type: Schema.Types.Mixed
},
// Timestamps
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
}, {
timestamps: true
});
// Compound index for faster queries
analyticsSchema.index({ entityId: 1, date: 1 });
analyticsSchema.index({ userId: 1, entityType: 1, date: 1 });
analyticsSchema.index({ entityType: 1, date: 1 });
/**
* Find analytics by entity ID
*/
analyticsSchema.statics.findByEntityId = function(entityId, startDate, endDate) {
const query = { entityId };
if (startDate || endDate) {
query.date = {};
if (startDate) query.date.$gte = startDate;
if (endDate) query.date.$lte = endDate;
}
return this.find(query).sort({ date: 1 });
};
/**
* Find analytics by user ID
*/
analyticsSchema.statics.findByUserId = function(userId, entityType, startDate, endDate) {
const query = { userId };
if (entityType) query.entityType = entityType;
if (startDate || endDate) {
query.date = {};
if (startDate) query.date.$gte = startDate;
if (endDate) query.date.$lte = endDate;
}
return this.find(query).sort({ date: 1 });
};
/**
* Find analytics by date range
*/
analyticsSchema.statics.findByDateRange = function(startDate, endDate, entityType) {
const query = {};
if (entityType) query.entityType = entityType;
if (startDate || endDate) {
query.date = {};
if (startDate) query.date.$gte = startDate;
if (endDate) query.date.$lte = endDate;
}
return this.find(query).sort({ date: 1 });
};
/**
* Calculate aggregate metrics for a specific entity
*/
analyticsSchema.statics.calculateAggregateMetrics = async function(entityId, startDate, endDate) {
const pipeline = [
{
$match: {
entityId,
date: { $gte: startDate, $lte: endDate }
}
},
{
$group: {
_id: '$entityId',
impressions: { $sum: '$impressions' },
reach: { $sum: '$reach' },
clicks: { $sum: '$clicks' },
spend: { $sum: '$spend' },
conversions: { $sum: '$conversions' },
// Calculate averages
ctr: { $avg: '$ctr' },
cpc: { $avg: '$cpc' },
cpm: { $avg: '$cpm' },
frequency: { $avg: '$frequency' },
conversionRate: { $avg: '$conversionRate' },
costPerConversion: { $avg: '$costPerConversion' },
roas: { $avg: '$roas' }
}
}
];
const results = await this.aggregate(pipeline);
return results.length > 0 ? results[0] : null;
};
/**
* Find or create analytics entry
*/
analyticsSchema.statics.findOrCreate = async function(entityId, entityType, userId, date, data) {
// Format date to start of day
const formattedDate = new Date(date);
formattedDate.setHours(0, 0, 0, 0);
// Find existing entry
let analytics = await this.findOne({
entityId,
entityType,
userId,
date: formattedDate
});
// Create new entry if not found
if (!analytics) {
analytics = new this({
entityId,
entityType,
userId,
date: formattedDate,
...data
});
} else {
// Update existing entry
Object.assign(analytics, data);
}
return analytics.save();
};
const Analytics = mongoose.model('Analytics', analyticsSchema);
module.exports = Analytics;