@memberjunction/actions-bizapps-lms
Version:
LMS system integration actions for MemberJunction
360 lines (326 loc) • 14.7 kB
text/typescript
import { RegisterClass } from '@memberjunction/global';
import { LearnWorldsBaseAction } from '../learnworlds-base.action';
import { ActionParam, ActionResultSimple, RunActionParams } from '@memberjunction/actions-base';
import { BaseAction } from '@memberjunction/actions';
/**
* Action to retrieve comprehensive analytics for a LearnWorlds course
*/
export class GetCourseAnalyticsAction extends LearnWorldsBaseAction {
/**
* Get course performance analytics
*/
public async InternalRunAction(params: RunActionParams): Promise<ActionResultSimple> {
const { Params, ContextUser } = params;
this.params = Params;
try {
// Extract and validate parameters
const courseId = this.getParamValue(Params, 'CourseID');
const dateFrom = this.getParamValue(Params, 'DateFrom');
const dateTo = this.getParamValue(Params, 'DateTo');
const includeUserBreakdown = this.getParamValue(Params, 'IncludeUserBreakdown') === true;
const includeModuleStats = this.getParamValue(Params, 'IncludeModuleStats') !== false;
const includeRevenue = this.getParamValue(Params, 'IncludeRevenue') !== false;
if (!courseId) {
return {
Success: false,
ResultCode: 'VALIDATION_ERROR',
Message: 'CourseID is required',
Params
};
}
// Build query parameters
const queryParams: any = {};
if (dateFrom) {
queryParams.date_from = new Date(dateFrom).toISOString().split('T')[0];
}
if (dateTo) {
queryParams.date_to = new Date(dateTo).toISOString().split('T')[0];
}
// Build query string
const queryString = Object.keys(queryParams).length > 0
? '?' + new URLSearchParams(queryParams).toString()
: '';
// Get course analytics
const analyticsResponse = await this.makeLearnWorldsRequest(
`/courses/${courseId}/analytics${queryString}`,
'GET',
null,
ContextUser
);
if (!analyticsResponse.success) {
return {
Success: false,
ResultCode: 'API_ERROR',
Message: analyticsResponse.message || 'Failed to retrieve analytics',
Params
};
}
const analyticsData = analyticsResponse.data || {};
// Format analytics data
const analytics: any = {
courseId: courseId,
period: {
from: dateFrom || 'all-time',
to: dateTo || 'current'
},
enrollment: {
totalEnrollments: analyticsData.total_enrollments || 0,
newEnrollments: analyticsData.new_enrollments || 0,
activeStudents: analyticsData.active_students || 0,
enrollmentTrend: analyticsData.enrollment_trend || []
},
progress: {
averageProgressPercentage: analyticsData.average_progress || 0,
completionRate: analyticsData.completion_rate || 0,
totalCompletions: analyticsData.total_completions || 0,
inProgressCount: analyticsData.in_progress_count || 0,
notStartedCount: analyticsData.not_started_count || 0,
dropoutRate: analyticsData.dropout_rate || 0
},
engagement: {
averageTimeSpent: analyticsData.average_time_spent || 0,
averageTimeSpentText: this.formatDuration(analyticsData.average_time_spent || 0),
totalTimeSpent: analyticsData.total_time_spent || 0,
totalTimeSpentText: this.formatDuration(analyticsData.total_time_spent || 0),
averageSessionDuration: analyticsData.average_session_duration || 0,
lastActivityDate: analyticsData.last_activity_date,
dailyActiveUsers: analyticsData.daily_active_users || []
},
performance: {
averageQuizScore: analyticsData.average_quiz_score || 0,
passRate: analyticsData.pass_rate || 0,
certificatesIssued: analyticsData.certificates_issued || 0,
averageTimeToComplete: analyticsData.average_time_to_complete || 0,
averageTimeToCompleteText: this.formatDuration(analyticsData.average_time_to_complete || 0)
}
};
// Get revenue analytics if requested
if (includeRevenue) {
const revenueResponse = await this.makeLearnWorldsRequest(
`/courses/${courseId}/revenue${queryString}`,
'GET',
null,
ContextUser
);
if (revenueResponse.success && revenueResponse.data) {
analytics.revenue = {
totalRevenue: revenueResponse.data.total_revenue || 0,
currency: revenueResponse.data.currency || 'USD',
averageOrderValue: revenueResponse.data.average_order_value || 0,
totalOrders: revenueResponse.data.total_orders || 0,
revenueTrend: revenueResponse.data.revenue_trend || [],
topMarkets: revenueResponse.data.top_markets || []
};
}
}
// Get module/lesson stats if requested
if (includeModuleStats) {
const moduleStatsResponse = await this.makeLearnWorldsRequest(
`/courses/${courseId}/modules/analytics`,
'GET',
null,
ContextUser
);
if (moduleStatsResponse.success && moduleStatsResponse.data) {
analytics.moduleStats = this.formatModuleStats(moduleStatsResponse.data.data || moduleStatsResponse.data);
}
}
// Get user breakdown if requested
if (includeUserBreakdown) {
const enrollmentQueryString = '?' + new URLSearchParams({ limit: '1000', include: 'progress' }).toString();
const userBreakdownResponse = await this.makeLearnWorldsRequest(
`/courses/${courseId}/enrollments${enrollmentQueryString}`,
'GET',
null,
ContextUser
);
if (userBreakdownResponse.success && userBreakdownResponse.data) {
const enrollments = userBreakdownResponse.data.data || userBreakdownResponse.data;
analytics.userBreakdown = this.calculateUserBreakdown(enrollments);
}
}
// Create summary
const summary = {
courseId: courseId,
period: analytics.period,
keyMetrics: {
totalEnrollments: analytics.enrollment.totalEnrollments,
completionRate: analytics.progress.completionRate,
averageProgress: analytics.progress.averageProgressPercentage,
activeStudents: analytics.enrollment.activeStudents,
averageTimeSpent: analytics.engagement.averageTimeSpentText,
certificatesIssued: analytics.performance.certificatesIssued
},
trends: {
enrollmentGrowth: this.calculateGrowthRate(analytics.enrollment.enrollmentTrend),
engagementTrend: this.calculateEngagementTrend(analytics.engagement.dailyActiveUsers),
completionTrend: 'stable' // Would calculate from historical data
}
};
// Update output parameters
const outputParams = [...Params];
const courseAnalyticsParam = outputParams.find(p => p.Name === 'CourseAnalytics');
if (courseAnalyticsParam) {
courseAnalyticsParam.Value = analytics;
}
const summaryParam = outputParams.find(p => p.Name === 'Summary');
if (summaryParam) {
summaryParam.Value = summary;
}
return {
Success: true,
ResultCode: 'SUCCESS',
Message: 'Course analytics retrieved successfully',
Params: outputParams
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
return {
Success: false,
ResultCode: 'EXECUTION_ERROR',
Message: `Error retrieving course analytics: ${errorMessage}`,
Params
};
}
}
/**
* Format module statistics
*/
private formatModuleStats(modules: any[]): any[] {
return modules.map(module => ({
moduleId: module.id,
moduleTitle: module.title,
completionRate: module.completion_rate || 0,
averageProgress: module.average_progress || 0,
averageTimeSpent: module.average_time_spent || 0,
averageTimeSpentText: this.formatDuration(module.average_time_spent || 0),
studentsStarted: module.students_started || 0,
studentsCompleted: module.students_completed || 0,
lessons: module.lessons?.map((lesson: any) => ({
lessonId: lesson.id,
lessonTitle: lesson.title,
completionRate: lesson.completion_rate || 0,
averageTimeSpent: lesson.average_time_spent || 0,
viewCount: lesson.view_count || 0
})) || []
}));
}
/**
* Calculate user breakdown statistics
*/
private calculateUserBreakdown(enrollments: any[]): any {
const progressBuckets = {
notStarted: 0,
under25: 0,
between25And50: 0,
between50And75: 0,
between75And99: 0,
completed: 0
};
enrollments.forEach(enrollment => {
const progress = enrollment.progress_percentage || 0;
if (progress === 0) progressBuckets.notStarted++;
else if (progress < 25) progressBuckets.under25++;
else if (progress < 50) progressBuckets.between25And50++;
else if (progress < 75) progressBuckets.between50And75++;
else if (progress < 100) progressBuckets.between75And99++;
else progressBuckets.completed++;
});
return {
total: enrollments.length,
progressDistribution: progressBuckets,
percentageDistribution: {
notStarted: (progressBuckets.notStarted / enrollments.length * 100).toFixed(1),
under25: (progressBuckets.under25 / enrollments.length * 100).toFixed(1),
between25And50: (progressBuckets.between25And50 / enrollments.length * 100).toFixed(1),
between50And75: (progressBuckets.between50And75 / enrollments.length * 100).toFixed(1),
between75And99: (progressBuckets.between75And99 / enrollments.length * 100).toFixed(1),
completed: (progressBuckets.completed / enrollments.length * 100).toFixed(1)
}
};
}
/**
* Calculate growth rate from trend data
*/
private calculateGrowthRate(trend: any[]): string {
if (!trend || trend.length < 2) return 'insufficient-data';
const recent = trend[trend.length - 1]?.value || 0;
const previous = trend[trend.length - 2]?.value || 0;
if (previous === 0) return 'new';
const growthRate = ((recent - previous) / previous) * 100;
if (growthRate > 10) return 'high-growth';
if (growthRate > 0) return 'growing';
if (growthRate === 0) return 'stable';
return 'declining';
}
/**
* Calculate engagement trend
*/
private calculateEngagementTrend(dailyActiveUsers: any[]): string {
if (!dailyActiveUsers || dailyActiveUsers.length < 7) return 'insufficient-data';
// Simple trend calculation based on last 7 days
const recentAvg = dailyActiveUsers.slice(-7).reduce((sum, day) => sum + (day.value || 0), 0) / 7;
const previousAvg = dailyActiveUsers.slice(-14, -7).reduce((sum, day) => sum + (day.value || 0), 0) / 7;
if (previousAvg === 0) return 'new';
const change = ((recentAvg - previousAvg) / previousAvg) * 100;
if (change > 5) return 'increasing';
if (change < -5) return 'decreasing';
return 'stable';
}
/**
* Define the parameters this action expects
*/
public get Params(): ActionParam[] {
const baseParams = this.getCommonLMSParams();
const specificParams: ActionParam[] = [
{
Name: 'CourseID',
Type: 'Input',
Value: null
},
{
Name: 'DateFrom',
Type: 'Input',
Value: null
},
{
Name: 'DateTo',
Type: 'Input',
Value: null
},
{
Name: 'IncludeUserBreakdown',
Type: 'Input',
Value: false
},
{
Name: 'IncludeModuleStats',
Type: 'Input',
Value: true
},
{
Name: 'IncludeRevenue',
Type: 'Input',
Value: true
},
{
Name: 'CourseAnalytics',
Type: 'Output',
Value: null
},
{
Name: 'Summary',
Type: 'Output',
Value: null
}
];
return [...baseParams, ...specificParams];
}
/**
* Metadata about this action
*/
public get Description(): string {
return 'Retrieves comprehensive analytics and performance metrics for a LearnWorlds course';
}
}