diff --git a/scripts/langindex.json b/scripts/langindex.json index 8a607bd94..523dd673a 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -104,10 +104,12 @@ "addon.calendar.currentmonth": "local_moodlemobileapp", "addon.calendar.daynext": "calendar", "addon.calendar.dayprev": "calendar", + "addon.calendar.dayviewtitle": "calendar", "addon.calendar.defaultnotificationtime": "local_moodlemobileapp", "addon.calendar.deleteallevents": "calendar", "addon.calendar.deleteevent": "calendar", "addon.calendar.deleteoneevent": "calendar", + "addon.calendar.detailedmonthviewtitle": "calendar", "addon.calendar.durationminutes": "calendar", "addon.calendar.durationnone": "calendar", "addon.calendar.durationuntil": "calendar", @@ -369,6 +371,7 @@ "addon.mod_assign.gradelocked": "assign", "addon.mod_assign.gradenotsynced": "local_moodlemobileapp", "addon.mod_assign.gradeoutof": "assign", + "addon.mod_assign.grading": "assign", "addon.mod_assign.gradingstatus": "assign", "addon.mod_assign.groupsubmissionsettings": "assign", "addon.mod_assign.hiddenuser": "assign", @@ -425,6 +428,7 @@ "addon.mod_assign.submittedlate": "assign", "addon.mod_assign.submittedovertime": "assign", "addon.mod_assign.submittedundertime": "assign", + "addon.mod_assign.subpagetitle": "assign", "addon.mod_assign.syncblockedusercomponent": "local_moodlemobileapp", "addon.mod_assign.timelimit": "assign", "addon.mod_assign.timemodified": "assign", @@ -2235,6 +2239,7 @@ "core.play": "local_moodlemobileapp", "core.previous": "moodle", "core.proceed": "moodle", + "core.publicprofile": "moodle", "core.pulltorefresh": "local_moodlemobileapp", "core.qrscanner": "local_moodlemobileapp", "core.question.answer": "question", diff --git a/src/addons/badges/pages/issued-badge/issued-badge.ts b/src/addons/badges/pages/issued-badge/issued-badge.ts index 890ccbfe0..1440e7f02 100644 --- a/src/addons/badges/pages/issued-badge/issued-badge.ts +++ b/src/addons/badges/pages/issued-badge/issued-badge.ts @@ -26,6 +26,8 @@ import { ActivatedRoute } from '@angular/router'; import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; import { AddonBadgesUserBadgesSource } from '@addons/badges/classes/user-badges-source'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreTime } from '@singletons/time'; /** * Page that displays the list of calendar events. @@ -38,6 +40,7 @@ export class AddonBadgesIssuedBadgePage implements OnInit, OnDestroy { protected badgeHash = ''; protected userId!: number; + protected logView: (badge: AddonBadgesUserBadge) => void; courseId = 0; user?: CoreUserProfile; @@ -58,6 +61,16 @@ export class AddonBadgesIssuedBadgePage implements OnInit, OnDestroy { ); this.badges = new CoreSwipeNavigationItemsManager(source); + + this.logView = CoreTime.once((badge) => { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'core_badges_view_user_badges', + name: badge.name, + data: { id: badge.uniquehash, category: 'badges' }, + url: `/badges/badge.php?hash=${badge.uniquehash}`, + }); + }); } /** @@ -105,6 +118,8 @@ export class AddonBadgesIssuedBadgePage implements OnInit, OnDestroy { this.course = undefined; } } + + this.logView(badge); } catch (message) { CoreDomUtils.showErrorModalDefault(message, 'Error getting badge data.'); } diff --git a/src/addons/badges/pages/user-badges/user-badges.ts b/src/addons/badges/pages/user-badges/user-badges.ts index 31ba89d84..9d02a2c5f 100644 --- a/src/addons/badges/pages/user-badges/user-badges.ts +++ b/src/addons/badges/pages/user-badges/user-badges.ts @@ -24,6 +24,9 @@ import { CoreNavigator } from '@services/navigator'; import { CoreListItemsManager } from '@classes/items-management/list-items-manager'; import { AddonBadgesUserBadgesSource } from '@addons/badges/classes/user-badges-source'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreTime } from '@singletons/time'; +import { Translate } from '@singletons'; /** * Page that displays the list of calendar events. @@ -39,6 +42,8 @@ export class AddonBadgesUserBadgesPage implements AfterViewInit, OnDestroy { @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; + protected logView: () => void; + constructor() { let courseId = CoreNavigator.getRouteNumberParam('courseId') ?? 0; // Use 0 for site badges. const userId = CoreNavigator.getRouteNumberParam('userId') ?? CoreSites.getCurrentSiteUserId(); @@ -52,6 +57,16 @@ export class AddonBadgesUserBadgesPage implements AfterViewInit, OnDestroy { CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(AddonBadgesUserBadgesSource, [courseId, userId]), AddonBadgesUserBadgesPage, ); + + this.logView = CoreTime.once(() => { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_badges_view_user_badges', + name: Translate.instant('addon.badges.badges'), + data: { courseId: this.badges.getSource().COURSE_ID, category: 'badges' }, + url: '/badges/mybadges.php', + }); + }); } /** @@ -95,6 +110,8 @@ export class AddonBadgesUserBadgesPage implements AfterViewInit, OnDestroy { try { await this.badges.reload(); + + this.logView(); } catch (message) { CoreDomUtils.showErrorModalDefault(message, 'Error loading badges'); diff --git a/src/addons/blog/pages/entries/entries.ts b/src/addons/blog/pages/entries/entries.ts index 11d2b32c6..b5ee60335 100644 --- a/src/addons/blog/pages/entries/entries.ts +++ b/src/addons/blog/pages/entries/entries.ts @@ -20,11 +20,14 @@ import { CoreMainMenuDeepLinkManager } from '@features/mainmenu/classes/deep-lin import { CoreTag } from '@features/tag/services/tag'; import { CoreUser, CoreUserProfile } from '@features/user/services/user'; import { IonRefresher } from '@ionic/angular'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; +import { CoreUrlUtils } from '@services/utils/url'; import { CoreUtils } from '@services/utils/utils'; +import { CoreTime } from '@singletons/time'; /** * Page that displays the list of blog entries. @@ -43,7 +46,7 @@ export class AddonBlogEntriesPage implements OnInit { protected canLoadMoreEntries = false; protected canLoadMoreUserEntries = true; protected siteHomeId: number; - protected fetchSuccess = false; + protected logView: () => void; loaded = false; canLoadMore = false; @@ -61,6 +64,25 @@ export class AddonBlogEntriesPage implements OnInit { constructor() { this.currentUserId = CoreSites.getCurrentSiteUserId(); this.siteHomeId = CoreSites.getCurrentSiteHomeId(); + + this.logView = CoreTime.once(async () => { + await CoreUtils.ignoreErrors(AddonBlog.logView(this.filter)); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_blog_view_entries', + name: this.title, + data: { + ...this.filter, + category: 'blog', + }, + url: CoreUrlUtils.addParamsToUrl('/blog/index.php', { + ...this.filter, + modid: this.filter.cmid, + cmid: undefined, + }), + }); + }); } /** @@ -200,10 +222,7 @@ export class AddonBlogEntriesPage implements OnInit { await Promise.all(promises); - if (!this.fetchSuccess) { - this.fetchSuccess = true; - CoreUtils.ignoreErrors(AddonBlog.logView(this.filter)); - } + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.blog.errorloadentries', true); this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading. diff --git a/src/addons/blog/services/blog.ts b/src/addons/blog/services/blog.ts index 83e588deb..c3491951e 100644 --- a/src/addons/blog/services/blog.ts +++ b/src/addons/blog/services/blog.ts @@ -14,7 +14,6 @@ import { Injectable } from '@angular/core'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; -import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; import { CoreTagItem } from '@features/tag/services/tag'; import { CoreSites } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; @@ -104,8 +103,6 @@ export class AddonBlogProvider { * @returns Promise to be resolved when done. */ async logView(filter: AddonBlogFilter = {}, siteId?: string): Promise { - CorePushNotifications.logViewListEvent('blog', 'core_blog_view_entries', filter, siteId); - const site = await CoreSites.getSite(siteId); const data: AddonBlogViewEntriesWSParams = { diff --git a/src/addons/calendar/components/calendar/calendar.ts b/src/addons/calendar/components/calendar/calendar.ts index 0295e9d15..1d9c2eea9 100644 --- a/src/addons/calendar/components/calendar/calendar.ts +++ b/src/addons/calendar/components/calendar/calendar.ts @@ -49,6 +49,10 @@ import { } from '@classes/items-management/swipe-slides-dynamic-items-manager-source'; import { CoreSwipeSlidesDynamicItemsManager } from '@classes/items-management/swipe-slides-dynamic-items-manager'; import moment from 'moment-timezone'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreUrlUtils } from '@services/utils/url'; +import { CoreTime } from '@singletons/time'; +import { Translate } from '@singletons'; /** * Component that displays a calendar. @@ -81,6 +85,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro // Observers and listeners. protected undeleteEventObserver: CoreEventObserver; protected managerUnsubscribe?: () => void; + protected logView: () => void; constructor(differs: KeyValueDiffers) { this.currentSiteId = CoreSites.getCurrentSiteId(); @@ -107,6 +112,29 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro this.hiddenDiffer = this.hidden; this.filterDiffer = differs.find(this.filter ?? {}).create(); + + this.logView = CoreTime.once(() => { + const month = this.manager?.getSelectedItem(); + if (!month) { + return; + } + + const params = { + course: this.filter?.courseId, + time: month.moment.unix(), + }; + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_calendar_get_calendar_monthly_view', + name: Translate.instant('addon.calendar.detailedmonthviewtitle', { $a: this.periodName }), + data: { + ...params, + category: 'calendar', + }, + url: CoreUrlUtils.addParamsToUrl('/calendar/view.php?view=month', params), + }); + }); } @HostBinding('attr.hidden') get hiddenAttribute(): string | null { @@ -124,7 +152,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro const source = new AddonCalendarMonthSlidesItemsManagerSource(this, moment({ year: this.initialYear, month: this.initialMonth ? this.initialMonth - 1 : undefined, - })); + }).startOf('month')); this.manager = new CoreSwipeSlidesDynamicItemsManager(source); this.managerUnsubscribe = this.manager.addListener({ onSelectedItemUpdated: (item) => { @@ -176,6 +204,8 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro await this.manager?.getSource().fetchData(); await this.manager?.getSource().load(this.manager?.getSelectedItem()); + + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); } diff --git a/src/addons/calendar/components/upcoming-events/upcoming-events.ts b/src/addons/calendar/components/upcoming-events/upcoming-events.ts index 4586fd259..909169ad8 100644 --- a/src/addons/calendar/components/upcoming-events/upcoming-events.ts +++ b/src/addons/calendar/components/upcoming-events/upcoming-events.ts @@ -25,6 +25,10 @@ import { AddonCalendarHelper, AddonCalendarFilter } from '../../services/calenda import { AddonCalendarOffline } from '../../services/calendar-offline'; import { CoreCategoryData, CoreCourses } from '@features/courses/services/courses'; import { CoreConstants } from '@/core/constants'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreUrlUtils } from '@services/utils/url'; +import { CoreTime } from '@singletons/time'; +import { Translate } from '@singletons'; /** * Component that displays upcoming events. @@ -54,6 +58,7 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, DoCheck, On protected lookAhead = 0; protected timeFormat?: string; protected differ: KeyValueDiffer; // To detect changes in the data input. + protected logView: () => void; // Observers. protected undeleteEventObserver: CoreEventObserver; @@ -84,6 +89,23 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, DoCheck, On ); this.differ = differs.find([]).create(); + + this.logView = CoreTime.once(() => { + const params = { + course: this.filter?.courseId, + }; + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_calendar_get_calendar_upcoming_view', + name: Translate.instant('addon.calendar.upcomingevents'), + data: { + ...params, + category: 'calendar', + }, + url: CoreUrlUtils.addParamsToUrl('/calendar/view.php?view=upcoming', params), + }); + }); } /** @@ -148,8 +170,9 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, DoCheck, On try { await Promise.all(promises); - this.fetchEvents(); + await this.fetchEvents(); + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); } diff --git a/src/addons/calendar/lang.json b/src/addons/calendar/lang.json index c448ad2db..8820fd715 100644 --- a/src/addons/calendar/lang.json +++ b/src/addons/calendar/lang.json @@ -11,10 +11,12 @@ "currentmonth": "Current Month", "daynext": "Next day", "dayprev": "Previous day", + "dayviewtitle": "Day view: {{$a}}", "defaultnotificationtime": "Default notification time", "deleteallevents": "Delete all events", "deleteevent": "Delete event", "deleteoneevent": "Delete this event", + "detailedmonthviewtitle": "Detailed month view: {{$a}}", "durationminutes": "Duration in minutes", "durationnone": "Without duration", "durationuntil": "Until", diff --git a/src/addons/calendar/pages/day/day.ts b/src/addons/calendar/pages/day/day.ts index 4ad52ea63..1f539005c 100644 --- a/src/addons/calendar/pages/day/day.ts +++ b/src/addons/calendar/pages/day/day.ts @@ -33,7 +33,7 @@ import { CoreCategoryData, CoreCourses, CoreEnrolledCourseData } from '@features import { CoreCoursesHelper } from '@features/courses/services/courses-helper'; import { AddonCalendarFilterComponent } from '../../components/filter/filter'; import moment from 'moment-timezone'; -import { NgZone } from '@singletons'; +import { NgZone, Translate } from '@singletons'; import { CoreNavigator } from '@services/navigator'; import { Params } from '@angular/router'; import { Subscription } from 'rxjs'; @@ -47,6 +47,9 @@ import { } from '@classes/items-management/swipe-slides-dynamic-items-manager-source'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; import { AddonCalendarEventsSource } from '@addons/calendar/classes/events-source'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreUrlUtils } from '@services/utils/url'; +import { CoreTime } from '@singletons/time'; /** * Page that displays the calendar events for a certain day. @@ -73,6 +76,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { protected onlineObserver: Subscription; protected filterChangedObserver: CoreEventObserver; protected managerUnsubscribe?: () => void; + protected logView: () => void; periodName?: string; manager?: CoreSwipeSlidesDynamicItemsManager; @@ -186,6 +190,28 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { this.isOnline = CoreNetwork.isOnline(); }); }); + + this.logView = CoreTime.once(() => { + const day = this.manager?.getSelectedItem(); + if (!day) { + return; + } + const params = { + course: this.filter.courseId, + time: day.moment.unix(), + }; + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_calendar_get_calendar_day_view', + name: Translate.instant('addon.calendar.dayviewtitle', { $a: this.periodName }), + data: { + ...params, + category: 'calendar', + }, + url: CoreUrlUtils.addParamsToUrl('/calendar/view.php?view=day', params), + }); + }); } /** @@ -209,7 +235,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { year: CoreNavigator.getRouteNumberParam('year'), month: month ? month - 1 : undefined, date: CoreNavigator.getRouteNumberParam('day'), - })); + }).startOf('day')); this.manager = new CoreSwipeSlidesDynamicItemsManager(source); this.managerUnsubscribe = this.manager.addListener({ onSelectedItemUpdated: (item) => { @@ -246,6 +272,8 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { await this.manager?.getSource().fetchData(this.filter.courseId); await this.manager?.getSource().load(this.manager?.getSelectedItem()); + + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); } @@ -500,6 +528,7 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte canCreate = false; protected dayPage: AddonCalendarDayPage; + protected sendLog = true; constructor(page: AddonCalendarDayPage, initialMoment: moment.Moment) { super({ moment: initialMoment }); @@ -780,6 +809,7 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte promises.push(AddonCalendar.invalidateTimeFormat()); this.categories = undefined; // Get categories again. + this.sendLog = true; if (selectedDay) { selectedDay.dirty = true; diff --git a/src/addons/competency/pages/competencies/competencies.page.ts b/src/addons/competency/pages/competencies/competencies.page.ts index 16eb9076a..e2c01de02 100644 --- a/src/addons/competency/pages/competencies/competencies.page.ts +++ b/src/addons/competency/pages/competencies/competencies.page.ts @@ -27,6 +27,9 @@ import { AddonCompetencyPlanCompetenciesSource } from '@addons/competency/classe import { AddonCompetencyCourseCompetenciesSource } from '@addons/competency/classes/competency-course-competencies-source'; import { CoreListItemsManager } from '@classes/items-management/list-items-manager'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreSites } from '@services/sites'; +import { CoreTime } from '@singletons/time'; /** * Page that displays the list of competencies of a learning plan. @@ -46,8 +49,11 @@ export class AddonCompetencyCompetenciesPage implements AfterViewInit, OnDestroy title = ''; + protected logView: () => void; + constructor() { const planId = CoreNavigator.getRouteNumberParam('planId'); + this.logView = CoreTime.once(() => this.performLogView()); if (!planId) { const courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); @@ -96,6 +102,8 @@ export class AddonCompetencyCompetenciesPage implements AfterViewInit, OnDestroy } else { this.title = Translate.instant('addon.competency.coursecompetencies'); } + + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error getting competencies data.'); } @@ -122,4 +130,42 @@ export class AddonCompetencyCompetenciesPage implements AfterViewInit, OnDestroy this.competencies.destroy(); } + /** + * Log view. + */ + protected performLogView(): void { + const source = this.competencies.getSource(); + + if (source instanceof AddonCompetencyPlanCompetenciesSource) { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'tool_lp_data_for_plan_page', + name: this.title, + data: { + category: 'competency', + planid: source.PLAN_ID, + }, + url: `/admin/tool/lp/plan.php?id=${source.PLAN_ID}`, + }); + + return; + } + + if (source.USER_ID && source.USER_ID !== CoreSites.getCurrentSiteUserId()) { + // Only log event when viewing own competencies. In LMS viewing students competencies uses a different view. + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'tool_lp_data_for_course_competencies_page', + name: this.title, + data: { + category: 'competency', + courseid: source.COURSE_ID, + }, + url: `/admin/tool/lp/coursecompetencies.php?courseid=${source.COURSE_ID}`, + }); + } + } diff --git a/src/addons/competency/pages/competency/competency.page.ts b/src/addons/competency/pages/competency/competency.page.ts index d3a9f825b..bf14914ac 100644 --- a/src/addons/competency/pages/competency/competency.page.ts +++ b/src/addons/competency/pages/competency/competency.page.ts @@ -27,6 +27,7 @@ import { AddonCompetency, AddonCompetencyDataForPlanPageCompetency, AddonCompetencyDataForCourseCompetenciesPageCompetency, + AddonCompetencyProvider, } from '@addons/competency/services/competency'; import { CoreNavigator } from '@services/navigator'; import { IonRefresher } from '@ionic/angular'; @@ -38,6 +39,9 @@ import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/ import { AddonCompetencyPlanCompetenciesSource } from '@addons/competency/classes/competency-plan-competencies-source'; import { ActivatedRouteSnapshot } from '@angular/router'; import { AddonCompetencyCourseCompetenciesSource } from '@addons/competency/classes/competency-course-competencies-source'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreUrlUtils } from '@services/utils/url'; /** * Page that displays the competency information. @@ -58,9 +62,11 @@ export class AddonCompetencyCompetencyPage implements OnInit, OnDestroy { contextLevel?: string; contextInstanceId?: number; - protected fetchSuccess = false; + protected logView: () => void; constructor() { + this.logView = CoreTime.once(() => this.performLogView()); + try { const planId = CoreNavigator.getRouteNumberParam('planId'); @@ -156,31 +162,7 @@ export class AddonCompetencyCompetencyPage implements OnInit, OnDestroy { } }); - if (!this.fetchSuccess) { - this.fetchSuccess = true; - const name = this.competency.competency.competency.shortname; - - if (source instanceof AddonCompetencyPlanCompetenciesSource) { - this.planStatus && await CoreUtils.ignoreErrors( - AddonCompetency.logCompetencyInPlanView( - source.PLAN_ID, - this.requireCompetencyId(), - this.planStatus, - name, - source.user?.id, - ), - ); - } else { - await CoreUtils.ignoreErrors( - AddonCompetency.logCompetencyInCourseView( - source.COURSE_ID, - this.requireCompetencyId(), - name, - source.USER_ID, - ), - ); - } - } + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error getting competency data.'); } @@ -288,6 +270,73 @@ export class AddonCompetencyCompetencyPage implements OnInit, OnDestroy { return competency.usercompetencysummary; } + /** + * Log view. + */ + protected async performLogView(): Promise { + if (!this.competency) { + return; + } + + const source = this.competencies.getSource(); + const compId = this.requireCompetencyId(); + const name = this.competency.competency.competency.shortname; + const userId = source.user?.id; + + if (source instanceof AddonCompetencyPlanCompetenciesSource) { + if (!this.planStatus) { + return; + } + + await CoreUtils.ignoreErrors( + AddonCompetency.logCompetencyInPlanView(source.PLAN_ID, compId, this.planStatus, name, userId), + ); + + const wsName = this.planStatus === AddonCompetencyProvider.STATUS_COMPLETE + ? 'core_competency_user_competency_plan_viewed' + : 'core_competency_user_competency_viewed_in_plan'; + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: wsName, + name, + data: { + id: compId, + category: 'competency', + planid: source.PLAN_ID, + planstatus: this.planStatus, + userid: userId, + }, + url: CoreUrlUtils.addParamsToUrl('/admin/tool/lp/user_competency_in_plan.php', { + planid: source.PLAN_ID, + userid: userId, + competencyid: compId, + }), + }); + + return; + } + + await CoreUtils.ignoreErrors(AddonCompetency.logCompetencyInCourseView(source.COURSE_ID, compId, name, source.USER_ID)); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'core_competency_user_competency_viewed_in_course', + name, + data: { + id: compId, + category: 'competency', + courseid: source.COURSE_ID, + userid: userId, + }, + url: CoreUrlUtils.addParamsToUrl('/admin/tool/lp/user_competency_in_course.php', { + courseid: source.COURSE_ID, + competencyid: compId, + userid: userId, + }), + }); + } + } /** diff --git a/src/addons/competency/pages/competencysummary/competencysummary.page.ts b/src/addons/competency/pages/competencysummary/competencysummary.page.ts index 28554cdb7..1495a9e6e 100644 --- a/src/addons/competency/pages/competencysummary/competencysummary.page.ts +++ b/src/addons/competency/pages/competencysummary/competencysummary.page.ts @@ -20,6 +20,8 @@ import { CoreNavigator } from '@services/navigator'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; import { ADDON_COMPETENCY_SUMMARY_PAGE } from '@addons/competency/competency.module'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays the competency summary. @@ -36,7 +38,30 @@ export class AddonCompetencyCompetencySummaryPage implements OnInit { contextLevel?: ContextLevel; contextInstanceId?: number; - protected fetchSuccess = false; // Whether a fetch was finished successfully. + protected logView: () => void; + + constructor() { + this.logView = CoreTime.once(async () => { + if (!this.competency) { + return; + } + + await CoreUtils.ignoreErrors( + AddonCompetency.logCompetencyView(this.competencyId, this.competency.competency.shortname), + ); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_competency_competency_viewed', + name: this.competency.competency.shortname, + data: { + competencyId: this.competencyId, + category: 'competency', + }, + url: `/admin/tool/lp/user_competency.php?id=${this.competencyId}`, + }); + }); + } /** * @inheritdoc @@ -77,10 +102,7 @@ export class AddonCompetencyCompetencySummaryPage implements OnInit { this.competency = result.competency; - if (!this.fetchSuccess) { - this.fetchSuccess = true; - CoreUtils.ignoreErrors(AddonCompetency.logCompetencyView(this.competencyId, this.competency.competency.shortname)); - } + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error getting competency summary data.'); } diff --git a/src/addons/competency/pages/coursecompetencies/coursecompetencies.page.ts b/src/addons/competency/pages/coursecompetencies/coursecompetencies.page.ts index d091bd470..cf780f27d 100644 --- a/src/addons/competency/pages/coursecompetencies/coursecompetencies.page.ts +++ b/src/addons/competency/pages/coursecompetencies/coursecompetencies.page.ts @@ -26,6 +26,10 @@ import { ADDON_COMPETENCY_SUMMARY_PAGE } from '@addons/competency/competency.mod import { CoreListItemsManager } from '@classes/items-management/list-items-manager'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; import { AddonCompetencyCourseCompetenciesSource } from '@addons/competency/classes/competency-course-competencies-source'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreSites } from '@services/sites'; +import { Translate } from '@singletons'; /** * Page that displays the list of competencies of a course. @@ -41,7 +45,11 @@ export class AddonCompetencyCourseCompetenciesPage implements OnInit, OnDestroy AddonCompetencyCourseCompetenciesSource >; + protected logView: () => void; + constructor() { + this.logView = CoreTime.once(() => this.performLogView()); + try { const courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); const userId = CoreNavigator.getRouteNumberParam('userId'); @@ -53,7 +61,6 @@ export class AddonCompetencyCourseCompetenciesPage implements OnInit, OnDestroy this.competencies = new CoreListItemsManager(source, AddonCompetencyCourseCompetenciesPage); } catch (error) { CoreDomUtils.showErrorModal(error); - CoreNavigator.back(); return; @@ -112,6 +119,8 @@ export class AddonCompetencyCourseCompetenciesPage implements OnInit, OnDestroy protected async fetchCourseCompetencies(): Promise { try { await this.competencies.getSource().reload(); + + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error getting course competencies data.'); } @@ -147,4 +156,26 @@ export class AddonCompetencyCourseCompetenciesPage implements OnInit, OnDestroy }); } + /** + * Log view. + */ + protected performLogView(): void { + const source = this.competencies.getSource(); + if (source.USER_ID && source.USER_ID !== CoreSites.getCurrentSiteUserId()) { + // Only log event when viewing own competencies. In LMS viewing students competencies uses a different view. + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'tool_lp_data_for_course_competencies_page', + name: Translate.instant('addon.competency.coursecompetencies'), + data: { + category: 'competency', + courseid: source.COURSE_ID, + }, + url: `/admin/tool/lp/coursecompetencies.php?courseid=${source.COURSE_ID}`, + }); + } + } diff --git a/src/addons/competency/pages/plan/plan.ts b/src/addons/competency/pages/plan/plan.ts index 85d06ca30..5c57a659b 100644 --- a/src/addons/competency/pages/plan/plan.ts +++ b/src/addons/competency/pages/plan/plan.ts @@ -23,6 +23,8 @@ import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/ import { AddonCompetencyPlansSource } from '@addons/competency/classes/competency-plans-source'; import { CoreListItemsManager } from '@classes/items-management/list-items-manager'; import { AddonCompetencyPlanCompetenciesSource } from '@addons/competency/classes/competency-plan-competencies-source'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreTime } from '@singletons/time'; /** * Page that displays a learning plan. @@ -36,7 +38,11 @@ export class AddonCompetencyPlanPage implements OnInit, OnDestroy { plans!: CoreSwipeNavigationItemsManager; competencies!: CoreListItemsManager; + protected logView: () => void; + constructor() { + this.logView = CoreTime.once(() => this.performLogView()); + try { const planId = CoreNavigator.getRequiredRouteNumberParam('planId'); const userId = CoreNavigator.getRouteNumberParam('userId'); @@ -93,6 +99,8 @@ export class AddonCompetencyPlanPage implements OnInit, OnDestroy { protected async fetchLearningPlan(): Promise { try { await this.competencies.getSource().reload(); + + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error getting learning plan data.'); } @@ -111,4 +119,26 @@ export class AddonCompetencyPlanPage implements OnInit, OnDestroy { }); } + /** + * Log view. + */ + protected performLogView(): void { + if (!this.plan) { + return; + } + + const planId = this.competencies.getSource().PLAN_ID; + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'tool_lp_data_for_plan_page', + name: this.plan.plan.name, + data: { + category: 'competency', + planid: planId, + }, + url: `/admin/tool/lp/coursecompetencies.php?id=${planId}`, + }); + } + } diff --git a/src/addons/competency/pages/planlist/planlist.ts b/src/addons/competency/pages/planlist/planlist.ts index eb314632a..991aa9690 100644 --- a/src/addons/competency/pages/planlist/planlist.ts +++ b/src/addons/competency/pages/planlist/planlist.ts @@ -20,6 +20,10 @@ import { CoreNavigator } from '@services/navigator'; import { AddonCompetencyPlanFormatted, AddonCompetencyPlansSource } from '@addons/competency/classes/competency-plans-source'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; import { CoreListItemsManager } from '@classes/items-management/list-items-manager'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreSites } from '@services/sites'; +import { Translate } from '@singletons'; /** * Page that displays the list of learning plans. @@ -34,11 +38,25 @@ export class AddonCompetencyPlanListPage implements AfterViewInit, OnDestroy { plans: CoreListItemsManager; + protected logView: () => void; + constructor() { const userId = CoreNavigator.getRouteNumberParam('userId'); const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(AddonCompetencyPlansSource, [userId]); this.plans = new CoreListItemsManager(source, AddonCompetencyPlanListPage); + + this.logView = CoreTime.once(async () => { + const userId = source.USER_ID ?? CoreSites.getCurrentSiteId(); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'tool_lp_data_for_plans_page', + name: Translate.instant('addon.competency.userplans'), + data: { userid: userId }, + url: `/admin/tool/lp/plans.php?userid=${userId}`, + }); + }); } /** @@ -58,6 +76,8 @@ export class AddonCompetencyPlanListPage implements AfterViewInit, OnDestroy { protected async fetchLearningPlans(): Promise { try { await this.plans.load(); + + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error getting learning plans data.'); } diff --git a/src/addons/competency/services/competency.ts b/src/addons/competency/services/competency.ts index 4991f034f..7f83cc0c2 100644 --- a/src/addons/competency/services/competency.ts +++ b/src/addons/competency/services/competency.ts @@ -16,7 +16,6 @@ import { Injectable } from '@angular/core'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; import { CoreCommentsArea } from '@features/comments/services/comments'; import { CoreCourseSummary, CoreCourseModuleSummary } from '@features/course/services/course'; -import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; import { CoreUserSummary } from '@features/user/services/user'; import { CoreSites } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; @@ -495,7 +494,7 @@ export class AddonCompetencyProvider { * @param planId ID of the plan. * @param competencyId ID of the competency. * @param planStatus Current plan Status to decide what action should be logged. - * @param name Name of the competency. + * @param name Deprecated, not used anymore. * @param userId User ID. If not defined, current user. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. @@ -525,12 +524,6 @@ export class AddonCompetencyProvider { ? 'core_competency_user_competency_plan_viewed' : 'core_competency_user_competency_viewed_in_plan'; - CorePushNotifications.logViewEvent(competencyId, name, 'competency', wsName, { - planid: planId, - planstatus: planStatus, - userid: userId, - }, siteId); - await site.write(wsName, params, preSets); } @@ -539,7 +532,7 @@ export class AddonCompetencyProvider { * * @param courseId ID of the course. * @param competencyId ID of the competency. - * @param name Name of the competency. + * @param name Deprecated, not used anymore. * @param userId User ID. If not defined, current user. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. @@ -564,14 +557,7 @@ export class AddonCompetencyProvider { typeExpected: 'boolean', }; - const wsName = 'core_competency_user_competency_viewed_in_course'; - - CorePushNotifications.logViewEvent(competencyId, name, 'competency', 'wsName', { - courseid: courseId, - userid: userId, - }, siteId); - - await site.write(wsName, params, preSets); + await site.write('core_competency_user_competency_viewed_in_course', params, preSets); } /** @@ -593,10 +579,7 @@ export class AddonCompetencyProvider { typeExpected: 'boolean', }; - const wsName = 'core_competency_competency_viewed'; - CorePushNotifications.logViewEvent(competencyId, name, 'competency', wsName, {}, siteId); - - await site.write(wsName, params, preSets); + await site.write('core_competency_competency_viewed', params, preSets); } } diff --git a/src/addons/coursecompletion/pages/report/report.ts b/src/addons/coursecompletion/pages/report/report.ts index b95bb88b5..b58921aa3 100644 --- a/src/addons/coursecompletion/pages/report/report.ts +++ b/src/addons/coursecompletion/pages/report/report.ts @@ -19,9 +19,12 @@ import { import { Component, OnInit } from '@angular/core'; import { CoreUser, CoreUserProfile } from '@features/user/services/user'; import { IonRefresher } from '@ionic/angular'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; +import { Translate } from '@singletons'; +import { CoreTime } from '@singletons/time'; /** * Page that displays the course completion report. @@ -33,6 +36,7 @@ import { CoreDomUtils } from '@services/utils/dom'; export class AddonCourseCompletionReportPage implements OnInit { protected userId!: number; + protected logView: () => void; courseId!: number; completionLoaded = false; @@ -42,6 +46,21 @@ export class AddonCourseCompletionReportPage implements OnInit { statusText?: string; user?: CoreUserProfile; + constructor() { + this.logView = CoreTime.once(() => { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'core_completion_get_course_completion_status', + name: Translate.instant('addon.coursecompletion.coursecompletion'), + data: { + course: this.courseId, + user: this.userId, + }, + url: `/blocks/completionstatus/details.php?course=${this.courseId}&user=${this.userId}`, + }); + }); + } + /** * @inheritdoc */ @@ -77,6 +96,7 @@ export class AddonCourseCompletionReportPage implements OnInit { this.showSelfComplete = AddonCourseCompletion.canMarkSelfCompleted(this.userId, this.completion); this.tracked = true; + this.logView(); } catch (error) { if (error && error.errorcode == 'notenroled') { // Not enrolled error, probably a teacher. diff --git a/src/addons/mod/assign/components/index/index.ts b/src/addons/mod/assign/components/index/index.ts index e63a1f1a5..638ba2215 100644 --- a/src/addons/mod/assign/components/index/index.ts +++ b/src/addons/mod/assign/components/index/index.ts @@ -57,7 +57,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo @ViewChild(AddonModAssignSubmissionComponent) submissionComponent?: AddonModAssignSubmissionComponent; component = AddonModAssignProvider.COMPONENT; - moduleName = 'assign'; + pluginName = 'assign'; assign?: AddonModAssignAssign; // The assign object. canViewAllSubmissions = false; // Whether the user can view all submissions. @@ -230,14 +230,20 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo return; // Shouldn't happen. } - await AddonModAssign.logView(this.assign.id, this.assign.name); + await CoreUtils.ignoreErrors(AddonModAssign.logView(this.assign.id)); + + this.analyticsLogEvent('mod_assign_view_assign'); if (this.canViewAllSubmissions) { // User can see all submissions, log grading view. - CoreUtils.ignoreErrors(AddonModAssign.logGradingView(this.assign.id, this.assign.name)); + await CoreUtils.ignoreErrors(AddonModAssign.logGradingView(this.assign.id)); + + this.analyticsLogEvent('mod_assign_view_grading_table', { sendUrl: false }); } else if (this.canViewOwnSubmission) { // User can only see their own submission, log view the user submission. - CoreUtils.ignoreErrors(AddonModAssign.logSubmissionView(this.assign.id, this.assign.name)); + await CoreUtils.ignoreErrors(AddonModAssign.logSubmissionView(this.assign.id)); + + this.analyticsLogEvent('mod_assign_view_submission_status', { sendUrl: false }); } } diff --git a/src/addons/mod/assign/lang.json b/src/addons/mod/assign/lang.json index de0b0cc9d..116924c0b 100644 --- a/src/addons/mod/assign/lang.json +++ b/src/addons/mod/assign/lang.json @@ -45,6 +45,7 @@ "gradelocked": "This grade is locked or overridden in the gradebook.", "gradenotsynced": "Grade not synced", "gradeoutof": "Grade out of {{$a}}", + "grading": "Grading", "gradingstatus": "Grading status", "groupsubmissionsettings": "Group submission settings", "hiddenuser": "Participant", @@ -101,6 +102,7 @@ "submittedlate": "Assignment was submitted {{$a}} late", "submittedovertime": "Assignment was submitted {{$a}} over the time limit", "submittedundertime": "Assignment was submitted {{$a}} under the time limit", + "subpagetitle": "{{$a.contextname}} - {{$a.subpage}}", "syncblockedusercomponent": "user grade", "timelimit": "Time limit", "timemodified": "Last modified", diff --git a/src/addons/mod/assign/pages/edit/edit.ts b/src/addons/mod/assign/pages/edit/edit.ts index 634d79773..b7677e451 100644 --- a/src/addons/mod/assign/pages/edit/edit.ts +++ b/src/addons/mod/assign/pages/edit/edit.ts @@ -39,6 +39,7 @@ import { AddonModAssignOffline } from '../../services/assign-offline'; import { AddonModAssignSync } from '../../services/assign-sync'; import { CoreUtils } from '@services/utils/utils'; import { CoreWSExternalFile } from '@services/ws'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that allows adding or editing an assigment submission. @@ -226,6 +227,17 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave { // No offline data found. this.hasOffline = false; } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_assign_save_submission', + name: Translate.instant('addon.mod_assign.subpagetitle', { + contextname: this.assign.name, + subpage: Translate.instant('addon.mod_assign.editsubmission'), + }), + data: { id: this.assign.id, category: 'assign' }, + url: `/mod/assign/view.php?action=editsubmission&id=${this.moduleId}`, + }); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error getting assigment data.'); diff --git a/src/addons/mod/assign/pages/submission-list/submission-list.ts b/src/addons/mod/assign/pages/submission-list/submission-list.ts index 1670f1dec..d0a59e2e0 100644 --- a/src/addons/mod/assign/pages/submission-list/submission-list.ts +++ b/src/addons/mod/assign/pages/submission-list/submission-list.ts @@ -34,6 +34,7 @@ import { AddonModAssignManualSyncData, AddonModAssignAutoSyncData, } from '../../services/assign-sync'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays a list of submissions of an assignment. @@ -168,6 +169,21 @@ export class AddonModAssignSubmissionListPage implements AfterViewInit, OnDestro protected async fetchAssignment(sync = false): Promise { try { await this.submissions.getSource().loadAssignment(sync); + + if (!this.assign) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'mod_assign_get_submissions', + name: Translate.instant('addon.mod_assign.subpagetitle', { + contextname: this.assign.name, + subpage: Translate.instant('addon.mod_assign.grading'), + }), + data: { assignid: this.assign.id, category: 'assign' }, + url: `/mod/assign/view.php?id=${this.assign.cmid}&action=grading`, + }); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error getting assigment data.'); } diff --git a/src/addons/mod/assign/pages/submission-review/submission-review.ts b/src/addons/mod/assign/pages/submission-review/submission-review.ts index 847d3502d..6749d9f1b 100644 --- a/src/addons/mod/assign/pages/submission-review/submission-review.ts +++ b/src/addons/mod/assign/pages/submission-review/submission-review.ts @@ -25,6 +25,9 @@ import { CoreDomUtils } from '@services/utils/dom'; import { AddonModAssignListFilterName, AddonModAssignSubmissionsSource } from '../../classes/submissions-source'; import { AddonModAssignSubmissionComponent } from '../../components/submission/submission'; import { AddonModAssign, AddonModAssignAssign } from '../../services/assign'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { Translate } from '@singletons'; /** * Page that displays a submission. @@ -49,8 +52,29 @@ export class AddonModAssignSubmissionReviewPage implements OnInit, OnDestroy, Ca protected assign?: AddonModAssignAssign; // The assignment the submission belongs to. protected blindMarking = false; // Whether it uses blind marking. protected forceLeave = false; // To allow leaving the page without checking for changes. + protected logView: () => void; - constructor(protected route: ActivatedRoute) { } + constructor(protected route: ActivatedRoute) { + this.logView = CoreTime.once(() => { + if (!this.assign) { + return; + } + + const id = this.blindMarking ? this.blindId : this.submitId; + const paramName = this.blindMarking ? 'blindid' : 'userid'; + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_assign_get_submission_status', + name: Translate.instant('addon.mod_assign.subpagetitle', { + contextname: this.assign.name, + subpage: Translate.instant('addon.mod_assign.grading'), + }), + data: { id, assignid: this.assign.id, category: 'assign' }, + url: `/mod/assign/view.php?id=${this.assign.cmid}&action=grader&${paramName}=${id}`, + }); + }); + } /** * @inheritdoc @@ -84,6 +108,7 @@ export class AddonModAssignSubmissionReviewPage implements OnInit, OnDestroy, Ca } this.fetchSubmission().finally(() => { + this.logView(); this.loaded = true; }); }); diff --git a/src/addons/mod/assign/services/assign.ts b/src/addons/mod/assign/services/assign.ts index c213cb8eb..ce34faf77 100644 --- a/src/addons/mod/assign/services/assign.ts +++ b/src/addons/mod/assign/services/assign.ts @@ -878,23 +878,19 @@ export class AddonModAssignProvider { * Report an assignment submission as being viewed. * * @param assignid Assignment ID. - * @param name Name of the assign. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - async logSubmissionView(assignid: number, name?: string, siteId?: string): Promise { + async logSubmissionView(assignid: number, siteId?: string): Promise { const params: AddonModAssignViewSubmissionStatusWSParams = { assignid, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_assign_view_submission_status', params, AddonModAssignProvider.COMPONENT, assignid, - name, - 'assign', - {}, siteId, ); } @@ -903,23 +899,19 @@ export class AddonModAssignProvider { * Report an assignment grading table is being viewed. * * @param assignid Assignment ID. - * @param name Name of the assign. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - async logGradingView(assignid: number, name?: string, siteId?: string): Promise { + async logGradingView(assignid: number, siteId?: string): Promise { const params: AddonModAssignViewGradingTableWSParams = { assignid, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_assign_view_grading_table', params, AddonModAssignProvider.COMPONENT, assignid, - name, - 'assign', - {}, siteId, ); } @@ -928,23 +920,19 @@ export class AddonModAssignProvider { * Report an assign as being viewed. * * @param assignid Assignment ID. - * @param name Name of the assign. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - async logView(assignid: number, name?: string, siteId?: string): Promise { + async logView(assignid: number, siteId?: string): Promise { const params: AddonModAssignViewAssignWSParams = { assignid, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_assign_view_assign', params, AddonModAssignProvider.COMPONENT, assignid, - name, - 'assign', - {}, siteId, ); } diff --git a/src/addons/mod/bigbluebuttonbn/components/index/index.ts b/src/addons/mod/bigbluebuttonbn/components/index/index.ts index 72ad86990..92914b8ae 100644 --- a/src/addons/mod/bigbluebuttonbn/components/index/index.ts +++ b/src/addons/mod/bigbluebuttonbn/components/index/index.ts @@ -44,7 +44,7 @@ import { export class AddonModBBBIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit { component = AddonModBBBService.COMPONENT; - moduleName = 'bigbluebuttonbn'; + pluginName = 'bigbluebuttonbn'; bbb?: AddonModBBBData; groupInfo?: CoreGroupInfo; groupId = 0; @@ -226,7 +226,9 @@ export class AddonModBBBIndexComponent extends CoreCourseModuleMainActivityCompo return; // Shouldn't happen. } - await AddonModBBB.logView(this.bbb.id, this.bbb.name); + await CoreUtils.ignoreErrors(AddonModBBB.logView(this.bbb.id)); + + this.analyticsLogEvent('mod_bigbluebuttonbn_view_bigbluebuttonbn'); } /** diff --git a/src/addons/mod/bigbluebuttonbn/services/bigbluebuttonbn.ts b/src/addons/mod/bigbluebuttonbn/services/bigbluebuttonbn.ts index a59218853..dd1e40225 100644 --- a/src/addons/mod/bigbluebuttonbn/services/bigbluebuttonbn.ts +++ b/src/addons/mod/bigbluebuttonbn/services/bigbluebuttonbn.ts @@ -276,23 +276,19 @@ export class AddonModBBBService { * Report a BBB as being viewed. * * @param id BBB instance ID. - * @param name Name of the BBB. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - async logView(id: number, name?: string, siteId?: string): Promise { + async logView(id: number, siteId?: string): Promise { const params: AddonModBBBViewBigBlueButtonBNWSParams = { bigbluebuttonbnid: id, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_bigbluebuttonbn_view_bigbluebuttonbn', params, AddonModBBBService.COMPONENT, id, - name, - 'bigbluebuttonbn', - {}, siteId, ); } diff --git a/src/addons/mod/book/components/index/index.ts b/src/addons/mod/book/components/index/index.ts index 93c64972f..aa77f2d33 100644 --- a/src/addons/mod/book/components/index/index.ts +++ b/src/addons/mod/book/components/index/index.ts @@ -19,6 +19,7 @@ import { CoreCourseContentsPage } from '@features/course/pages/contents/contents import { CoreCourse } from '@features/course/services/course'; import { CoreNavigator } from '@services/navigator'; import { AddonModBookModuleHandlerService } from '../../services/handlers/module'; +import { CoreUtils } from '@services/utils/utils'; /** * Component that displays a book entry page. @@ -29,6 +30,7 @@ import { AddonModBookModuleHandlerService } from '../../services/handlers/module }) export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy { + pluginName = 'book'; showNumbers = true; addPadding = true; showBullets = false; @@ -102,7 +104,9 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp * @inheritdoc */ protected async logActivity(): Promise { - AddonModBook.logView(this.module.instance, undefined, this.module.name); + await CoreUtils.ignoreErrors(AddonModBook.logView(this.module.instance)); + + this.analyticsLogEvent('mod_book_view_book'); } /** diff --git a/src/addons/mod/book/pages/contents/contents.ts b/src/addons/mod/book/pages/contents/contents.ts index 2bd8a6cbe..3d22145cf 100644 --- a/src/addons/mod/book/pages/contents/contents.ts +++ b/src/addons/mod/book/pages/contents/contents.ts @@ -40,6 +40,8 @@ import { AddonModBookProvider, AddonModBookTocChapter, } from '../../services/book'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreUrlUtils } from '@services/utils/url'; /** * Page that displays a book contents. @@ -286,7 +288,15 @@ export class AddonModBookContentsPage implements OnInit, OnDestroy { } // Chapter loaded, log view. - await CoreUtils.ignoreErrors(AddonModBook.logView(this.module.instance, chapterId, this.module.name)); + await CoreUtils.ignoreErrors(AddonModBook.logView(this.module.instance, chapterId)); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_book_view_book', + name: this.module.name, + data: { id: this.module.instance, category: 'book', chapterid: chapterId }, + url: CoreUrlUtils.addParamsToUrl(`/mod/book/view.php?id=${this.module.id}`, { chapterid: chapterId }), + }); const currentChapterIndex = this.chapters.findIndex((chapter) => chapter.id == chapterId); const isLastChapter = currentChapterIndex < 0 || this.chapters[currentChapterIndex + 1] === undefined; diff --git a/src/addons/mod/book/services/book.ts b/src/addons/mod/book/services/book.ts index 2365c5291..19a86c643 100644 --- a/src/addons/mod/book/services/book.ts +++ b/src/addons/mod/book/services/book.ts @@ -359,24 +359,20 @@ export class AddonModBookProvider { * * @param id Module ID. * @param chapterId Chapter ID. - * @param name Name of the book. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - async logView(id: number, chapterId?: number, name?: string, siteId?: string): Promise { + async logView(id: number, chapterId?: number, siteId?: string): Promise { const params: AddonModBookViewBookWSParams = { bookid: id, chapterid: chapterId, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_book_view_book', params, AddonModBookProvider.COMPONENT, id, - name, - 'book', - { chapterid: chapterId }, siteId, ); } diff --git a/src/addons/mod/chat/components/index/index.ts b/src/addons/mod/chat/components/index/index.ts index 7315bdf70..b298143c0 100644 --- a/src/addons/mod/chat/components/index/index.ts +++ b/src/addons/mod/chat/components/index/index.ts @@ -32,7 +32,7 @@ import { AddonModChatModuleHandlerService } from '../../services/handlers/module export class AddonModChatIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit { component = AddonModChatProvider.COMPONENT; - moduleName = 'chat'; + pluginName = 'chat'; chat?: AddonModChatChat; chatInfo?: { date: string; @@ -85,7 +85,9 @@ export class AddonModChatIndexComponent extends CoreCourseModuleMainActivityComp return; // Shouldn't happen. } - await AddonModChat.logView(this.chat.id, this.chat.name); + await AddonModChat.logView(this.chat.id); + + this.analyticsLogEvent('mod_chat_view_chat'); } /** diff --git a/src/addons/mod/chat/pages/chat/chat.ts b/src/addons/mod/chat/pages/chat/chat.ts index 8c13924cf..e1c168c10 100644 --- a/src/addons/mod/chat/pages/chat/chat.ts +++ b/src/addons/mod/chat/pages/chat/chat.ts @@ -28,6 +28,8 @@ import { Subscription } from 'rxjs'; import { AddonModChatUsersModalComponent, AddonModChatUsersModalResult } from '../../components/users-modal/users-modal'; import { AddonModChat, AddonModChatProvider, AddonModChatUser } from '../../services/chat'; import { AddonModChatFormattedMessage, AddonModChatHelper } from '../../services/chat-helper'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays a chat session. @@ -61,6 +63,7 @@ export class AddonModChatChatPage implements OnInit, OnDestroy, CanLeave { protected viewDestroyed = false; protected pollingRunning = false; protected users: AddonModChatUser[] = []; + protected logView: () => void; constructor() { this.currentUserId = CoreSites.getCurrentSiteUserId(); @@ -71,6 +74,16 @@ export class AddonModChatChatPage implements OnInit, OnDestroy, CanLeave { this.isOnline = CoreNetwork.isOnline(); }); }); + + this.logView = CoreTime.once(() => { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'mod_chat_get_chat_latest_messages', + name: this.title, + data: { chatid: this.chatId, category: 'chat' }, + url: `/mod/chat/gui_ajax/index.php?id=${this.chatId}`, + }); + }); } /** @@ -88,6 +101,7 @@ export class AddonModChatChatPage implements OnInit, OnDestroy, CanLeave { await this.fetchMessages(); this.startPolling(); + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_chat.errorwhileconnecting', true); CoreNavigator.back(); diff --git a/src/addons/mod/chat/pages/session-messages/session-messages.ts b/src/addons/mod/chat/pages/session-messages/session-messages.ts index c59e8af91..0e98536de 100644 --- a/src/addons/mod/chat/pages/session-messages/session-messages.ts +++ b/src/addons/mod/chat/pages/session-messages/session-messages.ts @@ -21,6 +21,9 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; import { AddonModChat } from '../../services/chat'; import { AddonModChatFormattedSessionMessage, AddonModChatHelper } from '../../services/chat-helper'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { Translate } from '@singletons'; +import { CoreTime } from '@singletons/time'; /** * Page that displays list of chat session messages. @@ -42,6 +45,19 @@ export class AddonModChatSessionMessagesPage implements OnInit { protected sessionStart!: number; protected sessionEnd!: number; protected groupId!: number; + protected logView: () => void; + + constructor() { + this.logView = CoreTime.once(() => { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'mod_chat_view_sessions', + name: Translate.instant('addon.mod_chat.messages'), + data: { chatid: this.chatId, category: 'chat' }, + url: `/mod/chat/report.php?id=${this.cmId}&start=${this.sessionStart}&end=${this.sessionEnd}`, + }); + }); + } /** * @inheritdoc diff --git a/src/addons/mod/chat/pages/sessions/sessions.ts b/src/addons/mod/chat/pages/sessions/sessions.ts index 9f78793fd..32f91b9fa 100644 --- a/src/addons/mod/chat/pages/sessions/sessions.ts +++ b/src/addons/mod/chat/pages/sessions/sessions.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { CoreListItemsManager } from '@classes/items-management/list-items-manager'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; @@ -21,6 +21,9 @@ import { CoreGroupInfo } from '@services/groups'; import { CoreNavigator } from '@services/navigator'; import { CoreDomUtils } from '@services/utils/dom'; import { AddonModChatSessionFormatted, AddonModChatSessionsSource } from '../../classes/chat-sessions-source'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreTime } from '@singletons/time'; +import { Translate } from '@singletons'; /** * Page that displays list of chat sessions. @@ -29,14 +32,32 @@ import { AddonModChatSessionFormatted, AddonModChatSessionsSource } from '../../ selector: 'page-addon-mod-chat-sessions', templateUrl: 'sessions.html', }) -export class AddonModChatSessionsPage implements AfterViewInit, OnDestroy { +export class AddonModChatSessionsPage implements OnInit, AfterViewInit, OnDestroy { @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; sessions!: CoreListItemsManager; courseId?: number; + protected logView: () => void; constructor() { + this.logView = CoreTime.once(() => { + const source = this.sessions.getSource(); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'mod_chat_view_sessions', + name: Translate.instant('addon.mod_chat.chatreport'), + data: { chatid: source.CHAT_ID, category: 'chat' }, + url: `/mod/chat/report.php?id=${source.CM_ID}`, + }); + }); + } + + /** + * @inheritdoc + */ + ngOnInit(): void { try { this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); const chatId = CoreNavigator.getRequiredRouteNumberParam('chatId'); @@ -91,6 +112,8 @@ export class AddonModChatSessionsPage implements AfterViewInit, OnDestroy { async fetchSessions(): Promise { try { await this.sessions.load(); + + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'core.errorloadingcontent', true); } diff --git a/src/addons/mod/chat/services/chat.ts b/src/addons/mod/chat/services/chat.ts index 4b508958f..2c631423a 100644 --- a/src/addons/mod/chat/services/chat.ts +++ b/src/addons/mod/chat/services/chat.ts @@ -87,23 +87,19 @@ export class AddonModChatProvider { * Report a chat as being viewed. * * @param id Chat instance ID. - * @param name Name of the chat. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - async logView(id: number, name?: string, siteId?: string): Promise { + async logView(id: number, siteId?: string): Promise { const params: AddonModChatViewChatWSParams = { chatid: id, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_chat_view_chat', params, AddonModChatProvider.COMPONENT, id, - name, - 'chat', - {}, siteId, ); } diff --git a/src/addons/mod/choice/components/index/index.ts b/src/addons/mod/choice/components/index/index.ts index d4f7e039c..e119299fa 100644 --- a/src/addons/mod/choice/components/index/index.ts +++ b/src/addons/mod/choice/components/index/index.ts @@ -48,7 +48,7 @@ import { AddonModChoicePrefetchHandler } from '../../services/handlers/prefetch' export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit { component = AddonModChoiceProvider.COMPONENT; - moduleName = 'choice'; + pluginName = 'choice'; choice?: AddonModChoiceChoice; options: AddonModChoiceOption[] = []; @@ -321,7 +321,9 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo return; // Shouldn't happen. } - await AddonModChoice.logView(this.choice.id, this.choice.name); + await AddonModChoice.logView(this.choice.id); + + this.analyticsLogEvent('mod_choice_view_choice'); } /** @@ -386,6 +388,8 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo this.checkCompletion(); } + this.analyticsLogEvent('mod_choice_view_choice', { data: { notify: 'choicesaved' } }); + await this.dataUpdated(online); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_choice.cannotsubmit', true); @@ -412,6 +416,8 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo this.content?.scrollToTop(); + this.analyticsLogEvent('mod_choice_view_choice', { data: { action: 'delchoice' } }); + // Refresh the data. Don't call dataUpdated because deleting an answer doesn't mark the choice as outdated. await this.refreshContent(false); } catch (error) { diff --git a/src/addons/mod/choice/services/choice.ts b/src/addons/mod/choice/services/choice.ts index 57e002fea..055b7affb 100644 --- a/src/addons/mod/choice/services/choice.ts +++ b/src/addons/mod/choice/services/choice.ts @@ -365,23 +365,19 @@ export class AddonModChoiceProvider { * Report the choice as being viewed. * * @param id Choice ID. - * @param name Name of the choice. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - logView(id: number, name?: string, siteId?: string): Promise { + logView(id: number, siteId?: string): Promise { const params: AddonModChoiceViewChoiceWSParams = { choiceid: id, }; - return CoreCourseLogHelper.logSingle( + return CoreCourseLogHelper.log( 'mod_choice_view_choice', params, AddonModChoiceProvider.COMPONENT, id, - name, - 'choice', - {}, siteId, ); } diff --git a/src/addons/mod/data/components/index/index.ts b/src/addons/mod/data/components/index/index.ts index 64e0ca22e..d412664f1 100644 --- a/src/addons/mod/data/components/index/index.ts +++ b/src/addons/mod/data/components/index/index.ts @@ -59,7 +59,7 @@ const contentToken = ''; export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit, OnDestroy { component = AddonModDataProvider.COMPONENT; - moduleName = 'data'; + pluginName = 'data'; access?: AddonModDataGetDataAccessInformationWSResponse; database?: AddonModDataData; @@ -420,8 +420,6 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp try { await this.fetchEntriesData(); - // Log activity view for coherence with Moodle web. - await this.logActivity(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true); } finally { @@ -470,9 +468,6 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp try { await this.fetchEntriesData(); - - // Log activity view for coherence with Moodle web. - return this.logActivity(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true); } @@ -535,7 +530,9 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp return; } - await AddonModData.logView(this.database.id, this.database.name); + await AddonModData.logView(this.database.id); + + this.analyticsLogEvent('mod_data_view_database'); } /** diff --git a/src/addons/mod/data/pages/edit/edit.ts b/src/addons/mod/data/pages/edit/edit.ts index e6452eab5..4946a6800 100644 --- a/src/addons/mod/data/pages/edit/edit.ts +++ b/src/addons/mod/data/pages/edit/edit.ts @@ -43,6 +43,8 @@ import { AddonModDataHelper } from '../../services/data-helper'; import { CoreDom } from '@singletons/dom'; import { AddonModDataEntryFieldInitialized } from '../../classes/base-field-plugin-component'; import { CoreTextUtils } from '@services/utils/text'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays the view edit page. @@ -65,6 +67,7 @@ export class AddonModDataEditPage implements OnInit { protected initialSelectedGroup?: number; protected isEditing = false; protected originalData: AddonModDataEntryFields = {}; + protected logView: () => void; entry?: AddonModDataEntry; fields: Record = {}; @@ -94,6 +97,20 @@ export class AddonModDataEditPage implements OnInit { constructor() { this.siteId = CoreSites.getCurrentSiteId(); this.editForm = new FormGroup({}); + + this.logView = CoreTime.once(() => { + if (!this.database) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: this.isEditing ? 'mod_data_update_entry' : 'mod_data_add_entry', + name: this.title, + data: { databaseid: this.database.id, category: 'data' }, + url: '/mod/data/edit.php?' + (this.isEditing ? `d=${this.database.id}&rid=${this.entryId}` : `id=${this.moduleId}`), + }); + }); } /** @@ -230,6 +247,7 @@ export class AddonModDataEditPage implements OnInit { } this.editFormRender = this.displayEditFields(); + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true); } diff --git a/src/addons/mod/data/pages/entry/entry.ts b/src/addons/mod/data/pages/entry/entry.ts index 681ffbda0..0fff2c131 100644 --- a/src/addons/mod/data/pages/entry/entry.ts +++ b/src/addons/mod/data/pages/entry/entry.ts @@ -36,6 +36,8 @@ import { AddonModDataProvider, } from '../../services/data'; import { AddonModDataHelper } from '../../services/data-helper'; import { AddonModDataSyncProvider } from '../../services/data-sync'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreTime } from '@singletons/time'; /** * Page that displays the view entry page. @@ -55,9 +57,9 @@ export class AddonModDataEntryPage implements OnInit, OnDestroy { protected entryChangedObserver: CoreEventObserver; // It will observe the changed entry event. protected fields: Record = {}; protected fieldsArray: AddonModDataField[] = []; - protected logAfterFetch = true; protected sortBy = 0; protected sortDirection = 'DESC'; + protected logView: () => void; moduleId = 0; courseId!: number; @@ -129,6 +131,8 @@ export class AddonModDataEntryPage implements OnInit, OnDestroy { } } }, this.siteId); + + this.logView = CoreTime.once(() => this.performLogView()); } /** @@ -219,13 +223,7 @@ export class AddonModDataEntryPage implements OnInit, OnDestroy { access: this.access, }; - if (this.logAfterFetch) { - this.logAfterFetch = false; - await CoreUtils.ignoreErrors(AddonModData.logView(this.database.id, this.database.name)); - - // Store module viewed because this page also updates recent accessed items block. - CoreCourse.storeModuleViewed(this.courseId, this.moduleId); - } + this.logView(); } catch (error) { if (!refresh) { // Some call failed, retry without using cache since it might be a new activity. @@ -250,7 +248,7 @@ export class AddonModDataEntryPage implements OnInit, OnDestroy { this.entryId = undefined; this.entry = undefined; this.entryLoaded = false; - this.logAfterFetch = true; + this.logView = CoreTime.once(() => this.performLogView()); // Log again after loading data. await this.fetchEntryData(); } @@ -310,7 +308,6 @@ export class AddonModDataEntryPage implements OnInit, OnDestroy { this.entry = undefined; this.entryId = undefined; this.entryLoaded = false; - this.logAfterFetch = true; await this.fetchEntryData(); } @@ -422,6 +419,28 @@ export class AddonModDataEntryPage implements OnInit, OnDestroy { AddonModData.invalidateEntryData(this.database!.id, this.entryId!); } + /** + * Log view. + */ + protected async performLogView(): Promise { + if (!this.database) { + return; + } + + await CoreUtils.ignoreErrors(AddonModData.logView(this.database.id)); + + // Store module viewed because this page also updates recent accessed items block. + CoreCourse.storeModuleViewed(this.courseId, this.moduleId); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_data_view_database', + name: this.database.name, + data: { id: this.entryId, databaseid: this.database.id, category: 'data' }, + url: `/mod/data/view.php?d=${this.database.id}&rid=${this.entryId}`, + }); + } + /** * @inheritdoc */ diff --git a/src/addons/mod/data/services/data.ts b/src/addons/mod/data/services/data.ts index 0ff632a17..a433b1bc0 100644 --- a/src/addons/mod/data/services/data.ts +++ b/src/addons/mod/data/services/data.ts @@ -955,23 +955,19 @@ export class AddonModDataProvider { * Report the database as being viewed. * * @param id Module ID. - * @param name Name of the data. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - async logView(id: number, name?: string, siteId?: string): Promise { + async logView(id: number, siteId?: string): Promise { const params: AddonModDataViewDatabaseWSParams = { databaseid: id, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_data_view_database', params, AddonModDataProvider.COMPONENT, id, - name, - 'data', - {}, siteId, ); } diff --git a/src/addons/mod/feedback/classes/feedback-attempts-source.ts b/src/addons/mod/feedback/classes/feedback-attempts-source.ts index fe3c6f74d..e08133127 100644 --- a/src/addons/mod/feedback/classes/feedback-attempts-source.ts +++ b/src/addons/mod/feedback/classes/feedback-attempts-source.ts @@ -37,8 +37,7 @@ export class AddonModFeedbackAttemptsSource extends CoreRoutedItemsManagerSource anonymous?: AddonModFeedbackWSAnonAttempt[]; anonymousTotal?: number; groupInfo?: CoreGroupInfo; - - protected feedback?: AddonModFeedbackWSFeedback; + feedback?: AddonModFeedbackWSFeedback; constructor(courseId: number, cmId: number) { super(); diff --git a/src/addons/mod/feedback/components/index/index.ts b/src/addons/mod/feedback/components/index/index.ts index e9e4defce..7980f3640 100644 --- a/src/addons/mod/feedback/components/index/index.ts +++ b/src/addons/mod/feedback/components/index/index.ts @@ -56,7 +56,7 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity @Input() group = 0; component = AddonModFeedbackProvider.COMPONENT; - moduleName = 'feedback'; + pluginName = 'feedback'; feedback?: AddonModFeedbackWSFeedback; goPage?: number; items: AddonModFeedbackItem[] = []; @@ -140,7 +140,18 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity return; // Shouldn't happen. } - await AddonModFeedback.logView(this.feedback.id, this.feedback.name); + await AddonModFeedback.logView(this.feedback.id); + + this.callAnalyticsLogEvent(); + } + + /** + * Call analytics. + */ + protected callAnalyticsLogEvent(): void { + this.analyticsLogEvent('mod_feedback_view_feedback', { + url: this.tab === 'analysis' ? `/mod/feedback/analysis.php?id=${this.module.id}` : undefined, + }); } /** @@ -429,11 +440,16 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity * @param tabName New tab name. */ tabChanged(tabName: string): void { + const tabHasChanged = this.tab !== undefined && this.tab !== tabName; this.tab = tabName; if (!this.tabsLoaded[this.tab]) { this.loadContent(false, false, true); } + + if (tabHasChanged) { + this.callAnalyticsLogEvent(); + } } /** diff --git a/src/addons/mod/feedback/pages/attempt/attempt.ts b/src/addons/mod/feedback/pages/attempt/attempt.ts index e2db40423..b80e42b79 100644 --- a/src/addons/mod/feedback/pages/attempt/attempt.ts +++ b/src/addons/mod/feedback/pages/attempt/attempt.ts @@ -27,6 +27,8 @@ import { AddonModFeedbackWSFeedback, } from '../../services/feedback'; import { AddonModFeedbackAttempt, AddonModFeedbackFormItem, AddonModFeedbackHelper } from '../../services/feedback-helper'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays a feedback attempt review. @@ -48,6 +50,7 @@ export class AddonModFeedbackAttemptPage implements OnInit, OnDestroy { loaded = false; protected attemptId: number; + protected logView: () => void; constructor() { this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId'); @@ -60,6 +63,21 @@ export class AddonModFeedbackAttemptPage implements OnInit, OnDestroy { ); this.attempts = new AddonModFeedbackAttemptsSwipeManager(source); + + this.logView = CoreTime.once(() => { + if (!this.feedback) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_feedback_get_responses_analysis', + name: this.feedback.name, + data: { id: this.attemptId, feedbackid: this.feedback.id, category: 'feedback' }, + url: `/mod/feedback/show_entries.php?id=${this.cmId}` + + (this.attempt ? `userid=${this.attempt.userid}` : '' ) + `&showcompleted=${this.attemptId}`, + }); + }); } /** @@ -129,6 +147,8 @@ export class AddonModFeedbackAttemptPage implements OnInit, OnDestroy { return attemptItem; }).filter((itemData) => itemData); // Filter items with errors. + + this.logView(); } catch (message) { // Some call failed on fetch, go back. CoreDomUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); diff --git a/src/addons/mod/feedback/pages/attempts/attempts.ts b/src/addons/mod/feedback/pages/attempts/attempts.ts index 3799bb66e..817fd2500 100644 --- a/src/addons/mod/feedback/pages/attempts/attempts.ts +++ b/src/addons/mod/feedback/pages/attempts/attempts.ts @@ -25,6 +25,8 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; import { AddonModFeedbackAttemptItem, AddonModFeedbackAttemptsSource } from '../../classes/feedback-attempts-source'; import { AddonModFeedbackWSAnonAttempt, AddonModFeedbackWSAttempt } from '../../services/feedback'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays feedback attempts. @@ -41,8 +43,25 @@ export class AddonModFeedbackAttemptsPage implements AfterViewInit, OnDestroy { fetchFailed = false; courseId?: number; + protected logView: () => void; + constructor(protected route: ActivatedRoute) { this.promisedAttempts = new CorePromisedValue(); + + this.logView = CoreTime.once(() => { + const source = this.attempts?.getSource(); + if (!source || !source.feedback) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'mod_feedback_get_responses_analysis', + name: source.feedback.name, + data: { feedbackid: source.feedback.id, category: 'feedback' }, + url: `/mod/feedback/show_entries.php?id=${source.CM_ID}`, + }); + }); } get attempts(): AddonModFeedbackAttemptsManager | null { @@ -112,6 +131,8 @@ export class AddonModFeedbackAttemptsPage implements AfterViewInit, OnDestroy { await attempts.getSource().loadFeedback(); await attempts.load(); + + this.logView(); } catch (error) { this.fetchFailed = true; diff --git a/src/addons/mod/feedback/pages/form/form.ts b/src/addons/mod/feedback/pages/form/form.ts index ad96737b0..0c45198a1 100644 --- a/src/addons/mod/feedback/pages/form/form.ts +++ b/src/addons/mod/feedback/pages/form/form.ts @@ -38,6 +38,7 @@ import { import { AddonModFeedbackFormItem, AddonModFeedbackHelper } from '../../services/feedback-helper'; import { AddonModFeedbackSync } from '../../services/feedback-sync'; import { AddonModFeedbackModuleHandlerService } from '../../services/handlers/module'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays feedback form. @@ -122,7 +123,7 @@ export class AddonModFeedbackFormPage implements OnInit, OnDestroy, CanLeave { } try { - await AddonModFeedback.logView(this.feedback.id, this.feedback.name, true); + await AddonModFeedback.logView(this.feedback.id, true); CoreCourse.checkModuleCompletion(this.courseId, this.module!.completiondata); } catch { @@ -263,6 +264,8 @@ export class AddonModFeedbackFormPage implements OnInit, OnDestroy, CanLeave { const itemsCopy = CoreUtils.clone(this.items); // Copy the array to avoid modifications. this.originalData = AddonModFeedbackHelper.getPageItemsResponses(itemsCopy); } + + this.analyticsLogEvent(); } /** @@ -435,6 +438,40 @@ export class AddonModFeedbackFormPage implements OnInit, OnDestroy, CanLeave { } } + /** + * Log event in analytics. + */ + protected analyticsLogEvent(): void { + if (!this.feedback) { + return; + } + + if (this.preview) { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_feedback_get_items', + name: this.feedback.name, + data: { id: this.feedback.id, category: 'feedback' }, + url: `/mod/feedback/print.php?id=${this.cmId}&courseid=${this.courseId}`, + }); + + return; + } + + let url = '/mod/feedback/complete.php'; + if (!this.completed) { + url += `?id=${this.cmId}` + (this.currentPage ? `&gopage=${this.currentPage}` : '') + `&courseid=${this.courseId}`; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: this.completed ? 'mod_feedback_get_feedback_access_information' : 'mod_feedback_get_page_items', + name: this.feedback.name, + data: { id: this.feedback.id, category: 'feedback', page: this.currentPage }, + url, + }); + } + /** * @inheritdoc */ diff --git a/src/addons/mod/feedback/pages/nonrespondents/nonrespondents.ts b/src/addons/mod/feedback/pages/nonrespondents/nonrespondents.ts index 6917682ba..2046106ff 100644 --- a/src/addons/mod/feedback/pages/nonrespondents/nonrespondents.ts +++ b/src/addons/mod/feedback/pages/nonrespondents/nonrespondents.ts @@ -20,6 +20,8 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; import { AddonModFeedback, AddonModFeedbackWSFeedback } from '../../services/feedback'; import { AddonModFeedbackHelper, AddonModFeedbackNonRespondent } from '../../services/feedback-helper'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays feedback non respondents. @@ -33,6 +35,7 @@ export class AddonModFeedbackNonRespondentsPage implements OnInit { protected cmId!: number; protected feedback?: AddonModFeedbackWSFeedback; protected page = 0; + protected logView: () => void; courseId!: number; selectedGroup!: number; @@ -43,6 +46,22 @@ export class AddonModFeedbackNonRespondentsPage implements OnInit { loaded = false; loadMoreError = false; + constructor() { + this.logView = CoreTime.once(() => { + if (!this.feedback) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'mod_feedback_get_non_respondents', + name: this.feedback.name, + data: { feedbackid: this.feedback.id, category: 'feedback' }, + url: `/mod/feedback/show_nonrespondents.php?id=${this.cmId}&courseid=${this.courseId}`, + }); + }); + } + /** * @inheritdoc */ @@ -81,6 +100,8 @@ export class AddonModFeedbackNonRespondentsPage implements OnInit { this.selectedGroup = CoreGroups.validateGroupId(this.selectedGroup, this.groupInfo); await this.loadGroupUsers(this.selectedGroup); + + this.logView(); } catch (message) { CoreDomUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); diff --git a/src/addons/mod/feedback/services/feedback.ts b/src/addons/mod/feedback/services/feedback.ts index c38e62ff8..8dd3fcf9d 100644 --- a/src/addons/mod/feedback/services/feedback.ts +++ b/src/addons/mod/feedback/services/feedback.ts @@ -1093,25 +1093,21 @@ export class AddonModFeedbackProvider { * Report the feedback as being viewed. * * @param id Module ID. - * @param name Name of the feedback. * @param formViewed True if form was viewed. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - async logView(id: number, name?: string, formViewed: boolean = false, siteId?: string): Promise { + async logView(id: number, formViewed: boolean = false, siteId?: string): Promise { const params: AddonModFeedbackViewFeedbackWSParams = { feedbackid: id, moduleviewed: formViewed, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_feedback_view_feedback', params, AddonModFeedbackProvider.COMPONENT, id, - name, - 'feedback', - { moduleviewed: params.moduleviewed }, siteId, ); } diff --git a/src/addons/mod/folder/components/index/index.ts b/src/addons/mod/folder/components/index/index.ts index 709d92815..40daf4693 100644 --- a/src/addons/mod/folder/components/index/index.ts +++ b/src/addons/mod/folder/components/index/index.ts @@ -22,6 +22,7 @@ import { Md5 } from 'ts-md5'; import { AddonModFolder, AddonModFolderFolder, AddonModFolderProvider } from '../../services/folder'; import { AddonModFolderFolderFormattedData, AddonModFolderHelper } from '../../services/folder-helper'; import { AddonModFolderModuleHandlerService } from '../../services/handlers/module'; +import { CoreUtils } from '@services/utils/utils'; /** * Component that displays a folder. @@ -39,6 +40,7 @@ export class AddonModFolderIndexComponent extends CoreCourseModuleMainResourceCo @Input() subfolder?: AddonModFolderFolderFormattedData; // Subfolder to show. component = AddonModFolderProvider.COMPONENT; + pluginName = 'folder'; contents?: AddonModFolderFolderFormattedData; constructor(@Optional() courseContentsPage?: CoreCourseContentsPage) { @@ -119,7 +121,9 @@ export class AddonModFolderIndexComponent extends CoreCourseModuleMainResourceCo * @inheritdoc */ protected async logActivity(): Promise { - await AddonModFolder.logView(this.module.instance, this.module.name); + await CoreUtils.ignoreErrors(AddonModFolder.logView(this.module.instance)); + + this.analyticsLogEvent('mod_folder_view_folder'); } /** diff --git a/src/addons/mod/folder/services/folder.ts b/src/addons/mod/folder/services/folder.ts index 701e0f56d..ab4f8f450 100644 --- a/src/addons/mod/folder/services/folder.ts +++ b/src/addons/mod/folder/services/folder.ts @@ -126,23 +126,19 @@ export class AddonModFolderProvider { * Report a folder as being viewed. * * @param id Module ID. - * @param name Name of the folder. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - async logView(id: number, name?: string, siteId?: string): Promise { + async logView(id: number, siteId?: string): Promise { const params: AddonModFolderViewFolderWSParams = { folderid: id, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_folder_view_folder', params, AddonModFolderProvider.COMPONENT, id, - name, - 'folder', - {}, siteId, ); } diff --git a/src/addons/mod/forum/components/index/index.ts b/src/addons/mod/forum/components/index/index.ts index bb604e3f2..3283edc33 100644 --- a/src/addons/mod/forum/components/index/index.ts +++ b/src/addons/mod/forum/components/index/index.ts @@ -71,7 +71,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; component = AddonModForumProvider.COMPONENT; - moduleName = 'forum'; + pluginName = 'forum'; descriptionNote?: string; promisedDiscussions: CorePromisedValue; discussionsItems: AddonModForumDiscussionItem[] = []; @@ -708,12 +708,14 @@ class AddonModForumDiscussionsManager extends CoreListItemsManager { + // Log analytics even if the user cancels for consistency with LMS. + this.analyticsLogEvent('mod_forum_delete_post', `/mod/forum/post.php?delete=${this.post.id}`); + try { await CoreDomUtils.showDeleteConfirm('addon.mod_forum.deletesure'); @@ -290,6 +294,8 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges } this.scrollToForm(); + + this.analyticsLogEvent('mod_forum_add_discussion_post', `/mod/forum/post.php?reply=${this.post.id}`); } /** @@ -314,6 +320,8 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges ); this.scrollToForm(); + + this.analyticsLogEvent('mod_forum_update_discussion_post', `/mod/forum/post.php?edit=${this.post.id}`); } catch { // Cancelled. } @@ -554,4 +562,24 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges ); } + /** + * Log analytics event. + * + * @param wsName WS name. + * @param url URL. + */ + protected analyticsLogEvent(wsName: string, url: string): void { + if (this.post.id <= 0) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: wsName, + name: this.post.subject, + data: { id: this.post.id, forumid: this.forum.id, category: 'forum' }, + url, + }); + } + } diff --git a/src/addons/mod/forum/pages/discussion/discussion.ts b/src/addons/mod/forum/pages/discussion/discussion.ts index bb06dcce1..60117a03f 100644 --- a/src/addons/mod/forum/pages/discussion/discussion.ts +++ b/src/addons/mod/forum/pages/discussion/discussion.ts @@ -51,6 +51,7 @@ import { import { AddonModForumHelper } from '../../services/forum-helper'; import { AddonModForumOffline } from '../../services/forum-offline'; import { AddonModForumSync, AddonModForumSyncProvider } from '../../services/forum-sync'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; type SortType = 'flat-newest' | 'flat-oldest' | 'nested'; @@ -562,19 +563,7 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes if (forceMarkAsRead || (hasUnreadPosts && this.trackPosts)) { // Add log in Moodle and mark unread posts as readed. - AddonModForum.logDiscussionView(this.discussionId, this.forumId || -1, this.forum.name).catch(() => { - // Ignore errors. - }).finally(() => { - if (!this.courseId || !this.cmId) { - return; - } - - // Trigger mark read posts. - CoreEvents.trigger(AddonModForumProvider.MARK_READ_EVENT, { - courseId: this.courseId, - moduleId: this.cmId, - }, CoreSites.getCurrentSiteId()); - }); + this.logDiscussionView(forceMarkAsRead); } } } @@ -854,6 +843,35 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes return posts; } + /** + * Log discussion as viewed. This will also mark the posts as read. + * + * @param logAnalytics Whether to log analytics too or not. + */ + protected async logDiscussionView(logAnalytics = false): Promise { + await CoreUtils.ignoreErrors(AddonModForum.logDiscussionView(this.discussionId, this.forumId || -1)); + + if (logAnalytics) { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_forum_view_forum_discussion', + name: this.startingPost?.subject ?? this.forum.name ?? '', + data: { id: this.discussionId, forumid: this.forumId, category: 'forum' }, + url: `/mod/forum/discuss.php?d=${this.discussionId}` + (this.postId ? `#p${this.postId}` : ''), + }); + } + + if (!this.courseId || !this.cmId) { + return; + } + + // Trigger mark read posts. + CoreEvents.trigger(AddonModForumProvider.MARK_READ_EVENT, { + courseId: this.courseId, + moduleId: this.cmId, + }, CoreSites.getCurrentSiteId()); + } + } /** diff --git a/src/addons/mod/forum/pages/new-discussion/new-discussion.ts b/src/addons/mod/forum/pages/new-discussion/new-discussion.ts index cdb7772e9..6978faa24 100644 --- a/src/addons/mod/forum/pages/new-discussion/new-discussion.ts +++ b/src/addons/mod/forum/pages/new-discussion/new-discussion.ts @@ -44,6 +44,8 @@ import { AddonModForumDiscussionsSwipeManager } from '../../classes/forum-discus import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; import { AddonModForumDiscussionsSource } from '../../classes/forum-discussions-source'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; type NewDiscussionData = { subject: string; @@ -105,8 +107,19 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea protected originalData?: Partial; protected forceLeave = false; protected initialGroupId?: number; + protected logView: () => void; - constructor(protected route: ActivatedRoute, @Optional() protected splitView: CoreSplitViewComponent) {} + constructor(protected route: ActivatedRoute, @Optional() protected splitView: CoreSplitViewComponent) { + this.logView = CoreTime.once(() => { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_forum_add_discussion', + name: Translate.instant('addon.mod_forum.addanewdiscussion'), + data: { id: this.forumId, category: 'forum' }, + url: '/mod/forum/post.php', + }); + }); + } /** * @inheritdoc @@ -309,6 +322,8 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea } this.showForm = true; + + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_forum.errorgetgroups', true); diff --git a/src/addons/mod/forum/services/forum.ts b/src/addons/mod/forum/services/forum.ts index d20f88efc..650dbefcc 100644 --- a/src/addons/mod/forum/services/forum.ts +++ b/src/addons/mod/forum/services/forum.ts @@ -996,23 +996,19 @@ export class AddonModForumProvider { * Report a forum as being viewed. * * @param id Module ID. - * @param name Name of the forum. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - logView(id: number, name?: string, siteId?: string): Promise { + logView(id: number, siteId?: string): Promise { const params = { forumid: id, }; - return CoreCourseLogHelper.logSingle( + return CoreCourseLogHelper.log( 'mod_forum_view_forum', params, AddonModForumProvider.COMPONENT, id, - name, - 'forum', - {}, siteId, ); } @@ -1022,23 +1018,19 @@ export class AddonModForumProvider { * * @param id Discussion ID. * @param forumId Forum ID. - * @param name Name of the forum. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - logDiscussionView(id: number, forumId: number, name?: string, siteId?: string): Promise { + logDiscussionView(id: number, forumId: number, siteId?: string): Promise { const params = { discussionid: id, }; - return CoreCourseLogHelper.logSingle( + return CoreCourseLogHelper.log( 'mod_forum_view_forum_discussion', params, AddonModForumProvider.COMPONENT, forumId, - name, - 'forum', - params, siteId, ); } diff --git a/src/addons/mod/glossary/components/index/index.ts b/src/addons/mod/glossary/components/index/index.ts index 3ca1498ca..053b507cf 100644 --- a/src/addons/mod/glossary/components/index/index.ts +++ b/src/addons/mod/glossary/components/index/index.ts @@ -71,7 +71,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; component = AddonModGlossaryProvider.COMPONENT; - moduleName = 'glossary'; + pluginName = 'glossary'; canAdd = false; loadMoreError = false; @@ -482,12 +482,14 @@ class AddonModGlossaryEntriesManager extends CoreListItemsManager void; + + constructor(@Optional() protected splitView: CoreSplitViewComponent, protected route: ActivatedRoute) { + this.logView = CoreTime.once(async () => { + if (!this.onlineEntry || !this.glossary || !this.componentId) { + return; + } + + await CoreUtils.ignoreErrors(AddonModGlossary.logEntryView(this.onlineEntry.id, this.componentId)); + + this.analyticsLogEvent('mod_glossary_get_entry_by_id', `/mod/glossary/showentry.php?eid=${this.onlineEntry.id}`); + }); + } get entry(): AddonModGlossaryEntry | AddonModGlossaryOfflineEntry | undefined { return this.onlineEntry ?? this.offlineEntry; @@ -128,12 +142,6 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { try { if (onlineEntryId) { await this.loadOnlineEntry(onlineEntryId); - - if (!this.glossary || !this.componentId) { - return; - } - - await CoreUtils.ignoreErrors(AddonModGlossary.logEntryView(onlineEntryId, this.componentId, this.glossary?.name)); } else if (offlineEntryTimeCreated) { await this.loadOfflineEntry(offlineEntryTimeCreated); } @@ -161,6 +169,12 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { * Delete entry. */ async deleteEntry(): Promise { + // Log analytics even if the user cancels for consistency with LMS. + this.analyticsLogEvent( + 'mod_glossary_delete_entry', + `/mod/glossary/deleteentry.php?id=${this.glossary?.id}&mode=delete&entry=${this.onlineEntry?.id}`, + ); + const glossaryId = this.glossary?.id; const cancelled = await CoreUtils.promiseFails( CoreDomUtils.showConfirm(Translate.instant('addon.mod_glossary.areyousuredelete')), @@ -250,6 +264,8 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { this.canEdit = canUpdateEntries && !!result.permissions?.canupdate; await this.loadGlossary(); + + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentry', true); } @@ -321,6 +337,26 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { AddonModGlossary.invalidateEntry(this.onlineEntry.id); } + /** + * Log analytics event. + * + * @param wsName WS name. + * @param url URL. + */ + protected analyticsLogEvent(wsName: string, url: string): void { + if (!this.onlineEntry || !this.glossary) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: wsName, + name: this.onlineEntry.concept, + data: { id: this.onlineEntry.id, glossaryid: this.glossary.id, category: 'glossary' }, + url, + }); + } + } /** diff --git a/src/addons/mod/glossary/services/glossary.ts b/src/addons/mod/glossary/services/glossary.ts index 487544639..fef56bab5 100644 --- a/src/addons/mod/glossary/services/glossary.ts +++ b/src/addons/mod/glossary/services/glossary.ts @@ -1020,23 +1020,19 @@ export class AddonModGlossaryProvider { * * @param glossaryId Glossary ID. * @param mode The mode in which the glossary was viewed. - * @param name Name of the glossary. * @param siteId Site ID. If not defined, current site. */ - async logView(glossaryId: number, mode: string, name?: string, siteId?: string): Promise { + async logView(glossaryId: number, mode: string, siteId?: string): Promise { const params: AddonModGlossaryViewGlossaryWSParams = { id: glossaryId, mode: mode, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_glossary_view_glossary', params, AddonModGlossaryProvider.COMPONENT, glossaryId, - name, - 'glossary', - { mode }, siteId, ); } @@ -1046,22 +1042,18 @@ export class AddonModGlossaryProvider { * * @param entryId Entry ID. * @param glossaryId Glossary ID. - * @param name Name of the glossary. * @param siteId Site ID. If not defined, current site. */ - async logEntryView(entryId: number, glossaryId: number, name?: string, siteId?: string): Promise { + async logEntryView(entryId: number, glossaryId: number, siteId?: string): Promise { const params: AddonModGlossaryViewEntryWSParams = { id: entryId, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_glossary_view_entry', params, AddonModGlossaryProvider.COMPONENT, glossaryId, - name, - 'glossary', - { entryid: entryId }, siteId, ); } diff --git a/src/addons/mod/h5pactivity/components/index/index.ts b/src/addons/mod/h5pactivity/components/index/index.ts index b84fc38bd..7af252102 100644 --- a/src/addons/mod/h5pactivity/components/index/index.ts +++ b/src/addons/mod/h5pactivity/components/index/index.ts @@ -63,7 +63,7 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv @Output() onActivityFinish = new EventEmitter(); component = AddonModH5PActivityProvider.COMPONENT; - moduleName = 'h5pactivity'; + pluginName = 'h5pactivity'; h5pActivity?: AddonModH5PActivityData; // The H5P activity object. accessInfo?: AddonModH5PActivityAccessInfo; // Info about the user capabilities. @@ -441,9 +441,11 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv this.playing = true; // Mark the activity as viewed. - await AddonModH5PActivity.logView(this.h5pActivity.id, this.h5pActivity.name, this.siteId); + await AddonModH5PActivity.logView(this.h5pActivity.id, this.siteId); this.checkCompletion(); + + this.analyticsLogEvent('mod_h5pactivity_view_h5pactivity'); } /** diff --git a/src/addons/mod/h5pactivity/pages/attempt-results/attempt-results.ts b/src/addons/mod/h5pactivity/pages/attempt-results/attempt-results.ts index e54f4017e..3142c9637 100644 --- a/src/addons/mod/h5pactivity/pages/attempt-results/attempt-results.ts +++ b/src/addons/mod/h5pactivity/pages/attempt-results/attempt-results.ts @@ -25,6 +25,8 @@ import { AddonModH5PActivityData, AddonModH5PActivityAttemptResults, } from '../../services/h5pactivity'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays results of an attempt. @@ -45,7 +47,28 @@ export class AddonModH5PActivityAttemptResultsPage implements OnInit { cmId!: number; protected attemptId!: number; - protected fetchSuccess = false; + protected logView: () => void; + + constructor() { + this.logView = CoreTime.once(async () => { + if (!this.h5pActivity) { + return; + } + + await CoreUtils.ignoreErrors(AddonModH5PActivity.logViewReport( + this.h5pActivity.id, + { attemptId: this.attemptId }, + )); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_h5pactivity_log_report_viewed', + name: this.h5pActivity.name, + data: { id: this.h5pActivity.id, attemptid: this.attemptId, category: 'h5pactivity' }, + url: `/mod/h5pactivity/report.php?a=${this.h5pActivity.id}&attemptid=${this.attemptId}`, + }); + }); + } /** * @inheritdoc @@ -92,14 +115,7 @@ export class AddonModH5PActivityAttemptResultsPage implements OnInit { await this.fetchUserProfile(); - if (!this.fetchSuccess) { - this.fetchSuccess = true; - CoreUtils.ignoreErrors(AddonModH5PActivity.logViewReport( - this.h5pActivity.id, - this.h5pActivity.name, - { attemptId: this.attemptId }, - )); - } + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error loading attempt.'); } finally { diff --git a/src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.ts b/src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.ts index e91c957bd..f0c2c9faf 100644 --- a/src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.ts +++ b/src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.ts @@ -26,6 +26,8 @@ import { AddonModH5PActivityData, AddonModH5PActivityUserAttempts, } from '../../services/h5pactivity'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays user attempts of a certain user. @@ -46,7 +48,28 @@ export class AddonModH5PActivityUserAttemptsPage implements OnInit { isCurrentUser = false; protected userId!: number; - protected fetchSuccess = false; + protected logView: () => void; + + constructor() { + this.logView = CoreTime.once(async () => { + if (!this.h5pActivity) { + return; + } + + await CoreUtils.ignoreErrors(AddonModH5PActivity.logViewReport( + this.h5pActivity.id, + { userId: this.userId }, + )); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_h5pactivity_log_report_viewed', + name: this.h5pActivity.name, + data: { id: this.h5pActivity.id, userid: this.userId, category: 'h5pactivity' }, + url: `/mod/h5pactivity/report.php?a=${this.h5pActivity.id}&userid=${this.userId}`, + }); + }); + } /** * @inheritdoc @@ -94,14 +117,7 @@ export class AddonModH5PActivityUserAttemptsPage implements OnInit { this.fetchUserProfile(), ]); - if (!this.fetchSuccess) { - this.fetchSuccess = true; - CoreUtils.ignoreErrors(AddonModH5PActivity.logViewReport( - this.h5pActivity.id, - this.h5pActivity.name, - { userId: this.userId }, - )); - } + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error loading attempts.'); } finally { diff --git a/src/addons/mod/h5pactivity/pages/users-attempts/users-attempts.ts b/src/addons/mod/h5pactivity/pages/users-attempts/users-attempts.ts index cf977abbf..f5eff6eb1 100644 --- a/src/addons/mod/h5pactivity/pages/users-attempts/users-attempts.ts +++ b/src/addons/mod/h5pactivity/pages/users-attempts/users-attempts.ts @@ -25,6 +25,8 @@ import { AddonModH5PActivityProvider, AddonModH5PActivityUserAttempts, } from '../../services/h5pactivity'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays all users that can attempt an H5P activity. @@ -45,7 +47,25 @@ export class AddonModH5PActivityUsersAttemptsPage implements OnInit { canLoadMore = false; protected page = 0; - protected fetchSuccess = false; + protected logView: () => void; + + constructor() { + this.logView = CoreTime.once(async () => { + if (!this.h5pActivity) { + return; + } + + await CoreUtils.ignoreErrors(AddonModH5PActivity.logViewReport(this.h5pActivity.id)); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'mod_h5pactivity_log_report_viewed', + name: this.h5pActivity.name, + data: { id: this.h5pActivity.id, category: 'h5pactivity' }, + url: `/mod/h5pactivity/report.php?a=${this.h5pActivity.id}`, + }); + }); + } /** * @inheritdoc @@ -90,10 +110,7 @@ export class AddonModH5PActivityUsersAttemptsPage implements OnInit { this.fetchUsers(refresh), ]); - if (!this.fetchSuccess) { - this.fetchSuccess = true; - CoreUtils.ignoreErrors(AddonModH5PActivity.logViewReport(this.h5pActivity.id, this.h5pActivity.name)); - } + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error loading attempts.'); } finally { diff --git a/src/addons/mod/h5pactivity/services/h5pactivity.ts b/src/addons/mod/h5pactivity/services/h5pactivity.ts index 594e0d201..5a23aa738 100644 --- a/src/addons/mod/h5pactivity/services/h5pactivity.ts +++ b/src/addons/mod/h5pactivity/services/h5pactivity.ts @@ -776,23 +776,19 @@ export class AddonModH5PActivityProvider { * Report an H5P activity as being viewed. * * @param id H5P activity ID. - * @param name Name of the activity. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - logView(id: number, name?: string, siteId?: string): Promise { + logView(id: number, siteId?: string): Promise { const params: AddonModH5PActivityViewH5pactivityWSParams = { h5pactivityid: id, }; - return CoreCourseLogHelper.logSingle( + return CoreCourseLogHelper.log( 'mod_h5pactivity_view_h5pactivity', params, AddonModH5PActivityProvider.COMPONENT, id, - name, - 'h5pactivity', - {}, siteId, ); } @@ -801,11 +797,10 @@ export class AddonModH5PActivityProvider { * Report an H5P activity report as being viewed. * * @param id H5P activity ID. - * @param name Name of the activity. * @param options Options. * @returns Promise resolved when the WS call is successful. */ - async logViewReport(id: number, name?: string, options: AddonModH5PActivityViewReportOptions = {}): Promise { + async logViewReport(id: number, options: AddonModH5PActivityViewReportOptions = {}): Promise { const site = await CoreSites.getSite(options.siteId); if (!site.wsAvailable('mod_h5pactivity_log_report_viewed')) { @@ -819,14 +814,11 @@ export class AddonModH5PActivityProvider { attemptid: options.attemptId, }; - return CoreCourseLogHelper.logSingle( + return CoreCourseLogHelper.log( 'mod_h5pactivity_log_report_viewed', params, AddonModH5PActivityProvider.COMPONENT, id, - name, - 'h5pactivity', - {}, site.getId(), ); } diff --git a/src/addons/mod/imscp/components/index/index.ts b/src/addons/mod/imscp/components/index/index.ts index bde54ea69..29e111c8a 100644 --- a/src/addons/mod/imscp/components/index/index.ts +++ b/src/addons/mod/imscp/components/index/index.ts @@ -18,6 +18,7 @@ import { CoreCourseContentsPage } from '@features/course/pages/contents/contents import { CoreCourse } from '@features/course/services/course'; import { CoreNavigator } from '@services/navigator'; import { AddonModImscpProvider, AddonModImscp, AddonModImscpTocItem } from '../../services/imscp'; +import { CoreUtils } from '@services/utils/utils'; /** * Component that displays a IMSCP. @@ -30,6 +31,7 @@ import { AddonModImscpProvider, AddonModImscp, AddonModImscpTocItem } from '../. export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit { component = AddonModImscpProvider.COMPONENT; + pluginName = 'imscp'; items: AddonModImscpTocItem[] = []; hasStarted = false; @@ -100,7 +102,9 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom * @inheritdoc */ protected async logActivity(): Promise { - await AddonModImscp.logView(this.module.instance, this.module.name); + await CoreUtils.ignoreErrors(AddonModImscp.logView(this.module.instance)); + + this.analyticsLogEvent('mod_imscp_view_imscp'); } /** diff --git a/src/addons/mod/imscp/services/imscp.ts b/src/addons/mod/imscp/services/imscp.ts index 4c388cf78..f71cf1193 100644 --- a/src/addons/mod/imscp/services/imscp.ts +++ b/src/addons/mod/imscp/services/imscp.ts @@ -271,7 +271,6 @@ export class AddonModImscpProvider { * Report a IMSCP as being viewed. * * @param id Module ID. - * @param name Name of the imscp. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ @@ -280,14 +279,11 @@ export class AddonModImscpProvider { imscpid: id, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_imscp_view_imscp', params, AddonModImscpProvider.COMPONENT, id, - name, - 'imscp', - {}, siteId, ); } diff --git a/src/addons/mod/lesson/components/index/index.ts b/src/addons/mod/lesson/components/index/index.ts index 7d89c2004..b4b6bcc98 100644 --- a/src/addons/mod/lesson/components/index/index.ts +++ b/src/addons/mod/lesson/components/index/index.ts @@ -66,7 +66,7 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo @Input() action?: string; // The "action" to display first. component = AddonModLessonProvider.COMPONENT; - moduleName = 'lesson'; + pluginName = 'lesson'; lesson?: AddonModLessonLessonWSData; // The lesson. selectedTab?: number; // The initial selected tab. @@ -372,7 +372,16 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo return; } - await AddonModLesson.logViewLesson(this.lesson.id, this.password, this.lesson.name); + await CoreUtils.ignoreErrors(AddonModLesson.logViewLesson(this.lesson.id, this.password)); + } + + /** + * Call analytics. + */ + protected callAnalyticsLogEvent(): void { + this.analyticsLogEvent('mod_lesson_view_lesson', { + url: this.selectedTab === 1 ? `/mod/lesson/report.php?id=${this.module.id}&action=reportoverview` : undefined, + }); } /** @@ -435,13 +444,19 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo * First tab selected. */ indexSelected(): void { + const tabHasChanged = this.selectedTab !== 0; this.selectedTab = 0; + + if (tabHasChanged) { + this.callAnalyticsLogEvent(); + } } /** * Reports tab selected. */ reportsSelected(): void { + const tabHasChanged = this.selectedTab !== 1; this.selectedTab = 1; if (!this.groupInfo) { @@ -449,6 +464,10 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo CoreDomUtils.showErrorModalDefault(error, 'Error getting report.'); }); } + + if (tabHasChanged) { + this.callAnalyticsLogEvent(); + } } /** diff --git a/src/addons/mod/lesson/pages/player/player.ts b/src/addons/mod/lesson/pages/player/player.ts index 2edc3daf7..99ca572a2 100644 --- a/src/addons/mod/lesson/pages/player/player.ts +++ b/src/addons/mod/lesson/pages/player/player.ts @@ -54,6 +54,7 @@ import { import { AddonModLessonOffline } from '../../services/lesson-offline'; import { AddonModLessonSync } from '../../services/lesson-sync'; import { CoreFormFields, CoreForms } from '@singletons/form'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that allows attempting and reviewing a lesson. @@ -446,6 +447,8 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy, CanLeave { this.reviewPageId = Number(params.pageid); } } + + this.logPageLoaded(AddonModLessonProvider.LESSON_EOL, Translate.instant('addon.mod_lesson.congratulations')); } /** @@ -615,6 +618,44 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy, CanLeave { } else { this.showRetake = false; } + + this.logPageLoaded(pageId, data.page?.title ?? ''); + } + + /** + * Log page loaded. + * + * @param pageId Page ID. + */ + protected logPageLoaded(pageId: number, title: string): void { + if (!this.lesson) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_lesson_get_page_data', + name: this.lesson.name + ': ' + title, + data: { id: this.lesson.id, pageid: pageId, category: 'lesson' }, + url: `/mod/lesson/view.php?id=${this.lesson.id}&pageid=${pageId}`, + }); + } + + /** + * Log continue page. + */ + protected logContinuePageLoaded(): void { + if (!this.lesson) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_lesson_process_page', + name: this.lesson.name + ': ' + Translate.instant('addon.mod_lesson.continue'), + data: { id: this.lesson.id, category: 'lesson' }, + url: '/mod/lesson/continue.php', + }); } /** @@ -715,6 +756,8 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy, CanLeave { pageId: result.newpageid, }); } + + this.logContinuePageLoaded(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error processing page'); } finally { diff --git a/src/addons/mod/lesson/pages/user-retake/user-retake.ts b/src/addons/mod/lesson/pages/user-retake/user-retake.ts index 792c6477d..7ea3706a7 100644 --- a/src/addons/mod/lesson/pages/user-retake/user-retake.ts +++ b/src/addons/mod/lesson/pages/user-retake/user-retake.ts @@ -35,6 +35,7 @@ import { } from '../../services/lesson'; import { AddonModLessonAnswerData, AddonModLessonHelper } from '../../services/lesson-helper'; import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays a retake made by a certain user. @@ -59,6 +60,11 @@ export class AddonModLessonUserRetakePage implements OnInit { protected userId?: number; // User ID to see the retakes. protected retakeNumber?: number; // Number of the initial retake to see. protected previousSelectedRetake?: number; // To be able to detect the previous selected retake when it has changed. + protected logView: () => void; + + constructor() { + this.logView = CoreTime.once(() => this.performLogView()); + } /** * @inheritdoc @@ -93,6 +99,8 @@ export class AddonModLessonUserRetakePage implements OnInit { try { await this.setRetake(retakeNumber); + + this.performLogView(); } catch (error) { this.selectedRetake = this.previousSelectedRetake ?? this.selectedRetake; CoreDomUtils.showErrorModal(CoreUtils.addDataNotDownloadedError(error, 'Error getting attempt.')); @@ -160,6 +168,8 @@ export class AddonModLessonUserRetakePage implements OnInit { this.student.profileimageurl = user?.profileimageurl; await this.setRetake(this.selectedRetake); + + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error getting data.', true); } @@ -243,6 +253,23 @@ export class AddonModLessonUserRetakePage implements OnInit { return formattedData; } + /** + * Log view. + */ + protected performLogView(): void { + if (!this.lesson) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_lesson_get_user_attempt', + name: this.lesson.name + ': ' + Translate.instant('addon.mod_lesson.detailedstats'), + data: { id: this.lesson.id, userid: this.userId, try: this.selectedRetake, category: 'lesson' }, + url: `/mod/lesson/report.php?id=${this.cmId}&action=reportdetail&userid=${this.userId}&try=${this.selectedRetake}`, + }); + } + } /** diff --git a/src/addons/mod/lesson/services/lesson.ts b/src/addons/mod/lesson/services/lesson.ts index 27e780f13..af0b49451 100644 --- a/src/addons/mod/lesson/services/lesson.ts +++ b/src/addons/mod/lesson/services/lesson.ts @@ -2951,11 +2951,10 @@ export class AddonModLessonProvider { * * @param id Module ID. * @param password Lesson password (if any). - * @param name Name of the assign. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - async logViewLesson(id: number, password?: string, name?: string, siteId?: string): Promise { + async logViewLesson(id: number, password?: string, siteId?: string): Promise { const params: AddonModLessonViewLessonWSParams = { lessonid: id, }; @@ -2964,14 +2963,11 @@ export class AddonModLessonProvider { params.password = password; } - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_lesson_view_lesson', params, AddonModLessonProvider.COMPONENT, id, - name, - 'lesson', - {}, siteId, ); } diff --git a/src/addons/mod/lti/components/index/index.ts b/src/addons/mod/lti/components/index/index.ts index 333f0bb48..c1870a741 100644 --- a/src/addons/mod/lti/components/index/index.ts +++ b/src/addons/mod/lti/components/index/index.ts @@ -30,7 +30,7 @@ import { AddonModLtiHelper } from '../../services/lti-helper'; export class AddonModLtiIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit { component = AddonModLtiProvider.COMPONENT; - moduleName = 'lti'; + pluginName = 'lti'; displayDescription = false; lti?: AddonModLtiLti; // The LTI object. diff --git a/src/addons/mod/lti/services/lti-helper.ts b/src/addons/mod/lti/services/lti-helper.ts index 2d5aaa00b..9b0ab35e7 100644 --- a/src/addons/mod/lti/services/lti-helper.ts +++ b/src/addons/mod/lti/services/lti-helper.ts @@ -22,6 +22,7 @@ import { CoreDomUtils } from '@services/utils/dom'; import { makeSingleton } from '@singletons'; import { CoreEvents } from '@singletons/events'; import { AddonModLti, AddonModLtiLti } from './lti'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Service that provides some helper functions for LTI. @@ -86,7 +87,7 @@ export class AddonModLtiHelperProvider { const launchData = await AddonModLti.getLtiLaunchData(lti.id); // "View" LTI without blocking the UI. - this.logViewAndCheckCompletion(courseId, module, lti.id, lti.name, siteId); + this.logViewAndCheckCompletion(courseId, module, lti.id, siteId); // Launch LTI. return AddonModLti.launch(launchData.endpoint, launchData.parameters); @@ -111,16 +112,23 @@ export class AddonModLtiHelperProvider { courseId: number, module: CoreCourseModuleData, ltiId: number, - name?: string, siteId?: string, ): Promise { try { - await AddonModLti.logView(ltiId, name, siteId); + await AddonModLti.logView(ltiId,siteId); CoreCourse.checkModuleCompletion(courseId, module.completiondata); } catch { // Ignore errors. } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_lti_view_lti', + name: module.name, + data: { id: module.instance, category: 'lti' }, + url: `/mod/lti/view.php?id=${module.id}`, + }); } } diff --git a/src/addons/mod/lti/services/lti.ts b/src/addons/mod/lti/services/lti.ts index 357510506..989432eab 100644 --- a/src/addons/mod/lti/services/lti.ts +++ b/src/addons/mod/lti/services/lti.ts @@ -272,14 +272,11 @@ export class AddonModLtiProvider { ltiid: id, }; - return CoreCourseLogHelper.logSingle( + return CoreCourseLogHelper.log( 'mod_lti_view_lti', params, AddonModLtiProvider.COMPONENT, id, - name, - 'lti', - {}, siteId, ); } diff --git a/src/addons/mod/page/components/index/index.ts b/src/addons/mod/page/components/index/index.ts index 957a67fe9..d139e0473 100644 --- a/src/addons/mod/page/components/index/index.ts +++ b/src/addons/mod/page/components/index/index.ts @@ -31,6 +31,7 @@ import { AddonModPageHelper } from '../../services/page-helper'; export class AddonModPageIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit { component = AddonModPageProvider.COMPONENT; + pluginName = 'page'; contents?: string; displayDescription = false; displayTimemodified = true; @@ -114,7 +115,9 @@ export class AddonModPageIndexComponent extends CoreCourseModuleMainResourceComp * @inheritdoc */ protected async logActivity(): Promise { - await AddonModPage.logView(this.module.instance, this.module.name); + await CoreUtils.ignoreErrors(AddonModPage.logView(this.module.instance)); + + this.analyticsLogEvent('mod_page_view_page'); } } diff --git a/src/addons/mod/page/services/page.ts b/src/addons/mod/page/services/page.ts index 6a266342c..19579a5e8 100644 --- a/src/addons/mod/page/services/page.ts +++ b/src/addons/mod/page/services/page.ts @@ -141,23 +141,19 @@ export class AddonModPageProvider { * Report a page as being viewed. * * @param pageid Module ID. - * @param name Name of the page. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - logView(pageid: number, name?: string, siteId?: string): Promise { + logView(pageid: number, siteId?: string): Promise { const params: AddonModPageViewPageWSParams = { pageid, }; - return CoreCourseLogHelper.logSingle( + return CoreCourseLogHelper.log( 'mod_page_view_page', params, AddonModPageProvider.COMPONENT, pageid, - name, - 'page', - {}, siteId, ); } diff --git a/src/addons/mod/quiz/components/index/index.ts b/src/addons/mod/quiz/components/index/index.ts index b9390ec91..cf5d867e5 100644 --- a/src/addons/mod/quiz/components/index/index.ts +++ b/src/addons/mod/quiz/components/index/index.ts @@ -57,7 +57,7 @@ import { export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit, OnDestroy { component = AddonModQuizProvider.COMPONENT; - moduleName = 'quiz'; + pluginName = 'quiz'; quiz?: AddonModQuizQuizData; // The quiz. now?: number; // Current time. syncTime?: string; // Last synchronization time. @@ -386,7 +386,9 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp return; // Shouldn't happen. } - await AddonModQuiz.logViewQuiz(this.quiz.id, this.quiz.name); + await CoreUtils.ignoreErrors(AddonModQuiz.logViewQuiz(this.quiz.id)); + + this.analyticsLogEvent('mod_quiz_view_quiz'); } /** diff --git a/src/addons/mod/quiz/pages/player/player.ts b/src/addons/mod/quiz/pages/player/player.ts index b70daf5f8..0ea6316c0 100644 --- a/src/addons/mod/quiz/pages/player/player.ts +++ b/src/addons/mod/quiz/pages/player/player.ts @@ -49,6 +49,7 @@ import { CoreDom } from '@singletons/dom'; import { CoreTime } from '@singletons/time'; import { CoreDirectivesRegistry } from '@singletons/directives-registry'; import { CoreWSError } from '@classes/errors/wserror'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that allows attempting a quiz. @@ -534,9 +535,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { // @todo MOBILE-4350: This is called before getting the attempt data in sequential quizzes as a workaround for a bug // in the LMS. Once the bug has been fixed, this should be reverted. if (this.isSequential) { - await CoreUtils.ignoreErrors( - AddonModQuiz.logViewAttempt(this.attempt.id, page, this.preflightData, this.offline, this.quiz), - ); + await this.logViewPage(page); } const data = await AddonModQuiz.getAttemptData(this.attempt.id, page, this.preflightData, { @@ -569,15 +568,55 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { // Mark the page as viewed. if (!this.isSequential) { // @todo MOBILE-4350: Undo workaround. - CoreUtils.ignoreErrors( - AddonModQuiz.logViewAttempt(this.attempt.id, page, this.preflightData, this.offline, this.quiz), - ); + await this.logViewPage(page); } // Start looking for changes. this.autoSave.startCheckChangesProcess(this.quiz, this.attempt, this.preflightData, this.offline); } + /** + * Log view a page. + * + * @param page Page viewed. + */ + protected async logViewPage(page: number): Promise { + if (!this.quiz || !this.attempt) { + return; + } + + await CoreUtils.ignoreErrors(AddonModQuiz.logViewAttempt(this.attempt.id, page, this.preflightData, this.offline)); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_quiz_view_attempt', + name: this.quiz.name, + data: { id: this.attempt.id, quizid: this.quiz.id, page, category: 'quiz' }, + url: `/mod/quiz/attempt.php?attempt=${this.attempt.id}&cmid=${this.cmId}` + (page > 0 ? `&page=${page}` : ''), + }); + } + + /** + * Log view summary. + */ + protected async logViewSummary(): Promise { + if (!this.quiz || !this.attempt) { + return; + } + + await CoreUtils.ignoreErrors( + AddonModQuiz.logViewAttemptSummary(this.attempt.id, this.preflightData, this.quiz.id), + ); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_quiz_view_attempt_summary', + name: this.quiz.name, + data: { id: this.attempt.id, quizid: this.quiz.id, category: 'quiz' }, + url: `/mod/quiz/summary.php?attempt=${this.attempt.id}&cmid=${this.cmId}`, + }); + } + /** * Refresh attempt data. */ @@ -618,10 +657,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { this.dueDateWarning = AddonModQuiz.getAttemptDueDateWarning(this.quiz, this.attempt); - // Log summary as viewed. - CoreUtils.ignoreErrors( - AddonModQuiz.logViewAttemptSummary(this.attempt.id, this.preflightData, this.quiz.id, this.quiz.name), - ); + this.logViewSummary(); } /** diff --git a/src/addons/mod/quiz/pages/review/review.ts b/src/addons/mod/quiz/pages/review/review.ts index a5c789a0c..63eeaf7d7 100644 --- a/src/addons/mod/quiz/pages/review/review.ts +++ b/src/addons/mod/quiz/pages/review/review.ts @@ -37,6 +37,7 @@ import { AddonModQuizWSAdditionalData, } from '../../services/quiz'; import { AddonModQuizHelper } from '../../services/quiz-helper'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that allows reviewing a quiz attempt. @@ -73,11 +74,15 @@ export class AddonModQuizReviewPage implements OnInit { protected attemptId!: number; // The attempt being reviewed. protected currentPage!: number; // The current page being reviewed. protected options?: AddonModQuizCombinedReviewOptions; // Review options. - protected fetchSuccess = false; + protected logView: () => void; constructor( protected elementRef: ElementRef, ) { + this.logView = CoreTime.once(() => this.performLogView(true, { + showAllDisabled: !this.showAll, + page: this.currentPage, + })); } /** @@ -127,6 +132,8 @@ export class AddonModQuizReviewPage implements OnInit { try { await this.loadPage(page); + + this.performLogView(false, { page }); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorgetquestions', true); } finally { @@ -156,12 +163,7 @@ export class AddonModQuizReviewPage implements OnInit { // Load questions. await this.loadPage(this.currentPage); - if (!this.fetchSuccess) { - this.fetchSuccess = true; - CoreUtils.ignoreErrors( - AddonModQuiz.logViewAttemptReview(this.attemptId, this.quiz.id, this.quiz.name), - ); - } + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorgetquiz', true); } @@ -325,11 +327,13 @@ export class AddonModQuizReviewPage implements OnInit { /** * Switch mode: all questions in same page OR one page at a time. */ - switchMode(): void { + async switchMode(): Promise { this.showAll = !this.showAll; // Load all questions or first page, depending on the mode. - this.loadPage(this.showAll ? -1 : 0); + await this.loadPage(this.showAll ? -1 : 0); + + this.performLogView(false, { showAllDisabled: !this.showAll }); } async openNavigation(): Promise { @@ -351,6 +355,37 @@ export class AddonModQuizReviewPage implements OnInit { this.changePage(modalData.page, modalData.slot); } + /** + * Perform log view. + * + * @param logInLMS Whether to log in LMS too or only in analytics. + * @param options Other options. + */ + protected async performLogView(logInLMS = false, options: LogViewOptions = {}): Promise { + if (!this.quiz) { + return; + } + + if (logInLMS) { + await CoreUtils.ignoreErrors(AddonModQuiz.logViewAttemptReview(this.attemptId, this.quiz.id)); + } + + let url = `/mod/quiz/review.php?attempt=${this.attemptId}&cmid=${this.cmId}`; + if (options.showAllDisabled) { + url += '&showall=0'; + } else if (options.page && options.page > 0) { + url += `&page=${ options.page}`; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_quiz_view_attempt_review', + name: this.quiz.name, + data: { id: this.attemptId, quizid: this.quiz.id, page: options.page, category: 'quiz' }, + url: url, + }); + } + } /** @@ -359,3 +394,8 @@ export class AddonModQuizReviewPage implements OnInit { type QuizQuestion = CoreQuestionQuestionParsed & { readableMark?: string; }; + +type LogViewOptions = { + page?: number; // Page being viewed (if viewing pages); + showAllDisabled?: boolean; // Whether the showAll option has just been disabled. +}; diff --git a/src/addons/mod/quiz/services/quiz-sync.ts b/src/addons/mod/quiz/services/quiz-sync.ts index 74871a22e..1dd59d96c 100644 --- a/src/addons/mod/quiz/services/quiz-sync.ts +++ b/src/addons/mod/quiz/services/quiz-sync.ts @@ -404,13 +404,11 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider if (!finish) { // Answers sent, now set the current page. - // Don't pass the quiz instance because we don't want to trigger a Firebase event in this case. await CoreUtils.ignoreErrors(AddonModQuiz.logViewAttempt( onlineAttempt.id, offlineAttempt.currentpage, preflightData, false, - undefined, siteId, )); } diff --git a/src/addons/mod/quiz/services/quiz.ts b/src/addons/mod/quiz/services/quiz.ts index 95fefe96c..3582a144a 100644 --- a/src/addons/mod/quiz/services/quiz.ts +++ b/src/addons/mod/quiz/services/quiz.ts @@ -21,7 +21,6 @@ import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreGradesFormattedItem, CoreGradesHelper } from '@features/grades/services/grades-helper'; -import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; import { CoreQuestion, CoreQuestionQuestionParsed, @@ -1535,7 +1534,6 @@ export class AddonModQuizProvider { * @param page Page number. * @param preflightData Preflight required data (like password). * @param offline Whether attempt is offline. - * @param quiz Quiz instance. If set, a Firebase event will be stored. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ @@ -1544,7 +1542,6 @@ export class AddonModQuizProvider { page: number = 0, preflightData: Record = {}, offline?: boolean, - quiz?: AddonModQuizQuizWSData, siteId?: string, ): Promise { const site = await CoreSites.getSite(siteId); @@ -1564,16 +1561,6 @@ export class AddonModQuizProvider { if (offline) { promises.push(AddonModQuizOffline.setAttemptCurrentPage(attemptId, page, site.getId())); } - if (quiz) { - CorePushNotifications.logViewEvent( - quiz.id, - quiz.name, - 'quiz', - 'mod_quiz_view_attempt', - { attemptid: attemptId, page }, - siteId, - ); - } await Promise.all(promises); } @@ -1583,23 +1570,19 @@ export class AddonModQuizProvider { * * @param attemptId Attempt ID. * @param quizId Quiz ID. - * @param name Name of the quiz. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - logViewAttemptReview(attemptId: number, quizId: number, name?: string, siteId?: string): Promise { + logViewAttemptReview(attemptId: number, quizId: number, siteId?: string): Promise { const params: AddonModQuizViewAttemptReviewWSParams = { attemptid: attemptId, }; - return CoreCourseLogHelper.logSingle( + return CoreCourseLogHelper.log( 'mod_quiz_view_attempt_review', params, AddonModQuizProvider.COMPONENT, quizId, - name, - 'quiz', - params, siteId, ); } @@ -1610,7 +1593,6 @@ export class AddonModQuizProvider { * @param attemptId Attempt ID. * @param preflightData Preflight required data (like password). * @param quizId Quiz ID. - * @param name Name of the quiz. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ @@ -1618,7 +1600,6 @@ export class AddonModQuizProvider { attemptId: number, preflightData: Record, quizId: number, - name?: string, siteId?: string, ): Promise { const params: AddonModQuizViewAttemptSummaryWSParams = { @@ -1630,14 +1611,11 @@ export class AddonModQuizProvider { ), }; - return CoreCourseLogHelper.logSingle( + return CoreCourseLogHelper.log( 'mod_quiz_view_attempt_summary', params, AddonModQuizProvider.COMPONENT, quizId, - name, - 'quiz', - { attemptid: attemptId }, siteId, ); } @@ -1646,23 +1624,19 @@ export class AddonModQuizProvider { * Report a quiz as being viewed. * * @param id Module ID. - * @param name Name of the quiz. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - logViewQuiz(id: number, name?: string, siteId?: string): Promise { + logViewQuiz(id: number, siteId?: string): Promise { const params: AddonModQuizViewQuizWSParams = { quizid: id, }; - return CoreCourseLogHelper.logSingle( + return CoreCourseLogHelper.log( 'mod_quiz_view_quiz', params, AddonModQuizProvider.COMPONENT, id, - name, - 'quiz', - {}, siteId, ); } diff --git a/src/addons/mod/resource/components/index/index.ts b/src/addons/mod/resource/components/index/index.ts index 3871a2798..0830bae6a 100644 --- a/src/addons/mod/resource/components/index/index.ts +++ b/src/addons/mod/resource/components/index/index.ts @@ -47,6 +47,7 @@ import { CorePlatform } from '@services/platform'; export class AddonModResourceIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy { component = AddonModResourceProvider.COMPONENT; + pluginName = 'resource'; mode = ''; src = ''; @@ -188,7 +189,9 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource * @inheritdoc */ protected async logActivity(): Promise { - await AddonModResource.logView(this.module.instance, this.module.name); + await CoreUtils.ignoreErrors(AddonModResource.logView(this.module.instance)); + + this.analyticsLogEvent('mod_resource_view_resource'); } /** diff --git a/src/addons/mod/resource/services/resource-helper.ts b/src/addons/mod/resource/services/resource-helper.ts index 11e75b041..e781f0b54 100644 --- a/src/addons/mod/resource/services/resource-helper.ts +++ b/src/addons/mod/resource/services/resource-helper.ts @@ -28,6 +28,7 @@ import { CoreUtilsOpenFileOptions } from '@services/utils/utils'; import { makeSingleton, Translate } from '@singletons'; import { CorePath } from '@singletons/path'; import { AddonModResource, AddonModResourceProvider } from './resource'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Service that provides helper functions for resources. @@ -206,6 +207,14 @@ export class AddonModResourceHelperProvider { } catch { // Ignore errors. } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_resource_view_resource', + name: module.name, + data: { id: module.instance, category: 'resource' }, + url: `/mod/resource/view.php?id=${module.id}`, + }); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_resource.errorwhileloadingthecontent', true); } finally { diff --git a/src/addons/mod/resource/services/resource.ts b/src/addons/mod/resource/services/resource.ts index 10c6e1063..2b590b2c6 100644 --- a/src/addons/mod/resource/services/resource.ts +++ b/src/addons/mod/resource/services/resource.ts @@ -146,23 +146,19 @@ export class AddonModResourceProvider { * Report the resource as being viewed. * * @param id Module ID. - * @param name Name of the resource. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - async logView(id: number, name?: string, siteId?: string): Promise { + async logView(id: number, siteId?: string): Promise { const params: AddonModResourceViewResourceWSParams = { resourceid: id, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_resource_view_resource', params, AddonModResourceProvider.COMPONENT, id, - name, - 'resource', - {}, siteId, ); } diff --git a/src/addons/mod/scorm/components/index/index.ts b/src/addons/mod/scorm/components/index/index.ts index 90e36e1ed..8fca36f6c 100644 --- a/src/addons/mod/scorm/components/index/index.ts +++ b/src/addons/mod/scorm/components/index/index.ts @@ -57,7 +57,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom @Input() autoPlayData?: AddonModScormAutoPlayData; // Data to use to play the SCORM automatically. component = AddonModScormProvider.COMPONENT; - moduleName = 'scorm'; + pluginName = 'scorm'; scorm?: AddonModScormScorm; // The SCORM object. currentOrganization: Partial & { identifier: string} = { @@ -361,7 +361,9 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom return; // Shouldn't happen. } - await AddonModScorm.logView(this.scorm.id, this.scorm.name); + await CoreUtils.ignoreErrors(AddonModScorm.logView(this.scorm.id)); + + this.analyticsLogEvent('mod_scorm_view_scorm'); } /** diff --git a/src/addons/mod/scorm/pages/player/player.ts b/src/addons/mod/scorm/pages/player/player.ts index 5e6e79137..aaa42569a 100644 --- a/src/addons/mod/scorm/pages/player/player.ts +++ b/src/addons/mod/scorm/pages/player/player.ts @@ -35,6 +35,7 @@ import { } from '../../services/scorm'; import { AddonModScormHelper, AddonModScormTOCScoWithIcon } from '../../services/scorm-helper'; import { AddonModScormSync } from '../../services/scorm-sync'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that allows playing a SCORM. @@ -442,8 +443,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { this.markCompleted(sco); } - // Trigger SCO launch event. - CoreUtils.ignoreErrors(AddonModScorm.logLaunchSco(this.scorm.id, sco.id, this.scorm.name)); + this.logEvent(sco.id); } /** @@ -581,6 +581,27 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { })); } + /** + * Log event. + */ + protected async logEvent(scoId: number): Promise { + await CoreUtils.ignoreErrors(AddonModScorm.logLaunchSco(this.scorm.id, scoId)); + + let url = '/mod/scorm/player.php'; + if (this.scorm.popup) { + url += `?a=${this.scorm.id}¤torg=${this.organizationId}&scoid=${scoId}` + + `&display=popup&mode=${this.mode}`; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_scorm_get_scorm_user_data', + name: this.scorm.name, + data: { id: this.scorm.id, scoid: scoId, organization: this.organizationId, category: 'scorm' }, + url, + }); + } + /** * @inheritdoc */ diff --git a/src/addons/mod/scorm/services/scorm.ts b/src/addons/mod/scorm/services/scorm.ts index 424aacfab..93e79b536 100644 --- a/src/addons/mod/scorm/services/scorm.ts +++ b/src/addons/mod/scorm/services/scorm.ts @@ -1420,24 +1420,20 @@ export class AddonModScormProvider { * * @param scormId SCORM ID. * @param scoId SCO ID. - * @param name Name of the SCORM. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - logLaunchSco(scormId: number, scoId: number, name?: string, siteId?: string): Promise { + logLaunchSco(scormId: number, scoId: number, siteId?: string): Promise { const params: AddonModScormLaunchScoWSParams = { scormid: scormId, scoid: scoId, }; - return CoreCourseLogHelper.logSingle( + return CoreCourseLogHelper.log( 'mod_scorm_launch_sco', params, AddonModScormProvider.COMPONENT, scormId, - name, - 'scorm', - { scoid: scoId }, siteId, ); } @@ -1446,23 +1442,19 @@ export class AddonModScormProvider { * Report a SCORM as being viewed. * * @param id Module ID. - * @param name Name of the SCORM. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - logView(id: number, name?: string, siteId?: string): Promise { + logView(id: number, siteId?: string): Promise { const params: AddonModScormViewScormWSParams = { scormid: id, }; - return CoreCourseLogHelper.logSingle( + return CoreCourseLogHelper.log( 'mod_scorm_view_scorm', params, AddonModScormProvider.COMPONENT, id, - name, - 'scorm', - {}, siteId, ); } diff --git a/src/addons/mod/survey/components/index/index.ts b/src/addons/mod/survey/components/index/index.ts index 8fa968dda..287830ed8 100644 --- a/src/addons/mod/survey/components/index/index.ts +++ b/src/addons/mod/survey/components/index/index.ts @@ -38,6 +38,7 @@ import { AddonModSurveySyncProvider, AddonModSurveySyncResult, } from '../../services/survey-sync'; +import { CoreUtils } from '@services/utils/utils'; /** * Component that displays a survey. @@ -50,7 +51,7 @@ import { export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit { component = AddonModSurveyProvider.COMPONENT; - moduleName = 'survey'; + pluginName = 'survey'; survey?: AddonModSurveySurvey; questions: AddonModSurveyQuestionFormatted[] = []; @@ -168,7 +169,9 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo return; // Shouldn't happen. } - await AddonModSurvey.logView(this.survey.id, this.survey.name); + await CoreUtils.ignoreErrors(AddonModSurvey.logView(this.survey.id)); + + this.analyticsLogEvent('mod_survey_view_survey'); } /** diff --git a/src/addons/mod/survey/services/survey.ts b/src/addons/mod/survey/services/survey.ts index 7ae3d4f52..4a7202356 100644 --- a/src/addons/mod/survey/services/survey.ts +++ b/src/addons/mod/survey/services/survey.ts @@ -208,7 +208,6 @@ export class AddonModSurveyProvider { * Report the survey as being viewed. * * @param id Module ID. - * @param name Name of the assign. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ @@ -217,14 +216,11 @@ export class AddonModSurveyProvider { surveyid: id, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_survey_view_survey', params, AddonModSurveyProvider.COMPONENT, id, - name, - 'survey', - {}, siteId, ); } diff --git a/src/addons/mod/url/components/index/index.ts b/src/addons/mod/url/components/index/index.ts index de2899f36..0d2cea0c1 100644 --- a/src/addons/mod/url/components/index/index.ts +++ b/src/addons/mod/url/components/index/index.ts @@ -34,6 +34,7 @@ import { AddonModUrlHelper } from '../../services/url-helper'; export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit { component = AddonModUrlProvider.COMPONENT; + pluginName = 'url'; url?: string; name?: string; @@ -153,12 +154,14 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo */ protected async logView(): Promise { try { - await AddonModUrl.logView(this.module.instance, this.module.name); + await AddonModUrl.logView(this.module.instance); this.checkCompletion(); } catch { // Ignore errors. } + + this.analyticsLogEvent('mod_url_view_url'); } /** diff --git a/src/addons/mod/url/services/handlers/module.ts b/src/addons/mod/url/services/handlers/module.ts index 60c044bf8..c4ea9f52a 100644 --- a/src/addons/mod/url/services/handlers/module.ts +++ b/src/addons/mod/url/services/handlers/module.ts @@ -26,6 +26,7 @@ import { makeSingleton } from '@singletons'; import { AddonModUrlIndexComponent } from '../../components/index/index'; import { AddonModUrl } from '../url'; import { AddonModUrlHelper } from '../url-helper'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Handler to support url modules. @@ -64,14 +65,7 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple * @param courseId The course ID. */ const openUrl = async (module: CoreCourseModuleData, courseId: number): Promise => { - try { - if (module.instance) { - await AddonModUrl.logView(module.instance, module.name); - CoreCourse.checkModuleCompletion(module.course, module.completiondata); - } - } catch { - // Ignore errors. - } + await this.logView(module); CoreCourse.storeModuleViewed(courseId, module.id); @@ -196,5 +190,27 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple return !iconUrl?.startsWith('assets/img/files/'); } + /** + * Log module viewed. + */ + protected async logView(module: CoreCourseModuleData): Promise { + try { + if (module.instance) { + await AddonModUrl.logView(module.instance); + CoreCourse.checkModuleCompletion(module.course, module.completiondata); + } + } catch { + // Ignore errors. + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_url_view_url', + name: module.name, + data: { id: module.instance, category: 'url' }, + url: `/mod/url/view.php?id=${module.id}`, + }); + } + } export const AddonModUrlModuleHandler = makeSingleton(AddonModUrlModuleHandlerService); diff --git a/src/addons/mod/url/services/url.ts b/src/addons/mod/url/services/url.ts index 404138cba..216ac680c 100644 --- a/src/addons/mod/url/services/url.ts +++ b/src/addons/mod/url/services/url.ts @@ -210,23 +210,19 @@ export class AddonModUrlProvider { * Report the url as being viewed. * * @param id Module ID. - * @param name Name of the assign. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - logView(id: number, name?: string, siteId?: string): Promise { + logView(id: number, siteId?: string): Promise { const params: AddonModUrlViewUrlWSParams = { urlid: id, }; - return CoreCourseLogHelper.logSingle( + return CoreCourseLogHelper.log( 'mod_url_view_url', params, AddonModUrlProvider.COMPONENT, id, - name, - 'url', - {}, siteId, ); } diff --git a/src/addons/mod/wiki/components/index/index.ts b/src/addons/mod/wiki/components/index/index.ts index f3d82dc3e..16dc33583 100644 --- a/src/addons/mod/wiki/components/index/index.ts +++ b/src/addons/mod/wiki/components/index/index.ts @@ -75,7 +75,7 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp component = AddonModWikiProvider.COMPONENT; componentId?: number; - moduleName = 'wiki'; + pluginName = 'wiki'; groupWiki = false; isOnline = false; @@ -327,9 +327,7 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp await this.showLoadingAndFetch(true, false); - if (this.currentPage && this.wiki) { - CoreUtils.ignoreErrors(AddonModWiki.logPageView(this.currentPage, this.wiki.id, this.wiki.name)); - } + this.currentPage && this.logPageViewed(this.currentPage); }, CoreSites.getCurrentSiteId()); } @@ -443,12 +441,60 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp return; // Shouldn't happen. } - if (!this.pageId) { - await AddonModWiki.logView(this.wiki.id, this.wiki.name); - } else { + if (this.pageId) { + // View page. this.checkCompletionAfterLog = false; - CoreUtils.ignoreErrors(AddonModWiki.logPageView(this.pageId, this.wiki.id, this.wiki.name)); + await this.logPageViewed(this.pageId); + + return; } + + await AddonModWiki.logView(this.wiki.id); + + if (this.groupId === undefined && this.userId === undefined) { + // View initial page. + this.analyticsLogEvent('mod_wiki_view_wiki', { name: this.currentPageObj?.title }); + + return; + } + + // Viewing a different subwiki. + const hasPersonalSubwikis = this.loadedSubwikis.some(subwiki => subwiki.userid > 0); + const hasGroupSubwikis = this.loadedSubwikis.some(subwiki => subwiki.groupid > 0); + + let url = `/mod/wiki/view.php?wid=${this.wiki.id}&title=${this.wiki.firstpagetitle}`; + if (hasPersonalSubwikis && hasGroupSubwikis) { + url += `&groupanduser=${this.groupId}-${this.userId}`; + } else if (hasPersonalSubwikis) { + url += `&uid=${this.userId}`; + } else { + url += `&group=${this.groupId}`; + } + + this.analyticsLogEvent('mod_wiki_view_wiki', { + name: this.currentPageObj?.title, + data: { subwiki: this.subwikiId, userid: this.userId, groupid: this.groupId }, + url, + }); + } + + /** + * Log page viewed. + * + * @param pageId Page ID. + */ + protected async logPageViewed(pageId: number): Promise { + if (!this.wiki) { + return; // Shouldn't happen. + } + + await CoreUtils.ignoreErrors(AddonModWiki.logPageView(pageId, this.wiki.id)); + + this.analyticsLogEvent('mod_wiki_view_page', { + name: this.currentPageObj?.title, + data: { pageid: this.pageId }, + url: `/mod/wiki/view.php?page=${this.pageId}`, + }); } /** @@ -619,7 +665,9 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp homeView: this.getWikiHomeView(), moduleId: this.module.id, courseId: this.courseId, + selectedId: this.currentPage, selectedTitle: this.currentPageObj && this.currentPageObj.title, + wiki: this.wiki, }, }); diff --git a/src/addons/mod/wiki/components/map/map.ts b/src/addons/mod/wiki/components/map/map.ts index 87cf410a0..9884350b0 100644 --- a/src/addons/mod/wiki/components/map/map.ts +++ b/src/addons/mod/wiki/components/map/map.ts @@ -15,7 +15,8 @@ import { Component, Input, OnInit } from '@angular/core'; import { ModalController } from '@singletons'; import { AddonModWikiPageDBRecord } from '../../services/database/wiki'; -import { AddonModWikiSubwikiPage } from '../../services/wiki'; +import { AddonModWikiSubwikiPage, AddonModWikiWiki } from '../../services/wiki'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Modal to display the map of a Wiki. @@ -27,6 +28,8 @@ import { AddonModWikiSubwikiPage } from '../../services/wiki'; export class AddonModWikiMapModalComponent implements OnInit { @Input() pages: (AddonModWikiSubwikiPage | AddonModWikiPageDBRecord)[] = []; + @Input() wiki?: AddonModWikiWiki; + @Input() selectedId?: number; @Input() selectedTitle?: string; @Input() moduleId?: number; @Input() courseId?: number; @@ -39,6 +42,16 @@ export class AddonModWikiMapModalComponent implements OnInit { */ ngOnInit(): void { this.constructMap(); + + if (this.selectedId && this.wiki) { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_wiki_get_subwiki_pages', + name: this.selectedTitle || this.wiki.name, + data: { id: this.wiki.id, pageid: this.selectedId, category: 'wiki' }, + url: `/mod/wiki/map.php?pageid=${this.selectedId}`, + }); + } } /** diff --git a/src/addons/mod/wiki/pages/edit/edit.ts b/src/addons/mod/wiki/pages/edit/edit.ts index 6c2c1b96a..86bd71e26 100644 --- a/src/addons/mod/wiki/pages/edit/edit.ts +++ b/src/addons/mod/wiki/pages/edit/edit.ts @@ -30,6 +30,7 @@ import { CoreForms } from '@singletons/form'; import { AddonModWiki, AddonModWikiProvider } from '../../services/wiki'; import { AddonModWikiOffline } from '../../services/wiki-offline'; import { AddonModWikiSync } from '../../services/wiki-sync'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that allows adding or editing a wiki page. @@ -64,6 +65,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave { protected editOffline = false; // Whether the user is editing an offline page. protected subwikiFiles: CoreWSFile[] = []; // List of files of the subwiki. protected originalContent?: string; // The original page content. + protected originalTitle?: string; // The original page title. protected version?: number; // Page version. protected renewLockInterval?: number; // An interval to renew the lock every certain time. protected forceLeave = false; // To allow leaving the page without checking for changes. @@ -89,17 +91,17 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave { this.groupId = CoreNavigator.getRouteNumberParam('groupId'); this.userId = CoreNavigator.getRouteNumberParam('userId'); - let pageTitle = CoreNavigator.getRouteParam('pageTitle'); - pageTitle = pageTitle ? CoreTextUtils.cleanTags(pageTitle.replace(/\+/g, ' '), { singleLine: true }) : ''; + const pageTitle = CoreNavigator.getRouteParam('pageTitle'); + this.originalTitle = pageTitle ? CoreTextUtils.cleanTags(pageTitle.replace(/\+/g, ' '), { singleLine: true }) : ''; - this.canEditTitle = !pageTitle; - this.title = pageTitle ? - Translate.instant('addon.mod_wiki.editingpage', { $a: pageTitle }) : + this.canEditTitle = !this.originalTitle; + this.title = this.originalTitle ? + Translate.instant('addon.mod_wiki.editingpage', { $a: this.originalTitle }) : Translate.instant('addon.mod_wiki.newpagehdr'); this.blockId = AddonModWikiSync.getSubwikiBlockId(this.subwikiId, this.wikiId, this.userId, this.groupId); // Create the form group and its controls. - this.pageForm.addControl('title', this.formBuilder.control(pageTitle)); + this.pageForm.addControl('title', this.formBuilder.control(this.originalTitle)); this.pageForm.addControl('text', this.contentControl); // Block the wiki so it cannot be synced. @@ -111,8 +113,8 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave { if (this.section) { this.editorExtraParams.section = this.section; } - } else if (pageTitle) { - this.editorExtraParams.pagetitle = pageTitle; + } else if (this.originalTitle) { + this.editorExtraParams.pagetitle = this.originalTitle; } try { @@ -126,6 +128,8 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave { this.blockId = newBlockId; CoreSync.blockOperation(this.component, this.blockId); } + + this.logView(); } } finally { this.loaded = true; @@ -158,6 +162,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave { this.wikiId = pageContents.wikiid; this.subwikiId = pageContents.subwikiid; this.title = Translate.instant('addon.mod_wiki.editingpage', { $a: pageContents.title }); + this.originalTitle = pageContents.title; this.groupId = pageContents.groupid; this.userId = pageContents.userid; canEdit = pageContents.caneditpage; @@ -466,6 +471,34 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave { } } + /** + * Log view. + */ + protected logView(): void { + let url: string; + if (this.pageId) { + url = `/mod/wiki/edit.php?pageid=${this.pageId}` + + (this.section ? `§ion=${this.section.replace(/ /g, '+')}` : ''); + } else if (this.originalTitle) { + const title = this.originalTitle.replace(/ /g, '+'); + if (this.subwikiId) { + url = `/mod/wiki/create.php?swid=${this.subwikiId}&title=${title}&action=new`; + } else { + url = `/mod/wiki/create.php?wid=${this.wikiId}&group=${this.groupId ?? 0}&uid=${this.userId ?? 0}&title=${title}`; + } + } else { + url = `/mod/wiki/create.php?action=new&wid=${this.wikiId}&swid=${this.subwikiId}`; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: this.pageId ? 'mod_wiki_edit_page' : 'mod_wiki_new_page', + name: this.originalTitle ?? Translate.instant('addon.mod_wiki.newpagehdr'), + data: { id: this.wikiId, subwiki: this.subwikiId, category: 'wiki' }, + url, + }); + } + /** * @inheritdoc */ diff --git a/src/addons/mod/wiki/services/wiki.ts b/src/addons/mod/wiki/services/wiki.ts index 62932720c..dc79309eb 100644 --- a/src/addons/mod/wiki/services/wiki.ts +++ b/src/addons/mod/wiki/services/wiki.ts @@ -621,23 +621,19 @@ export class AddonModWikiProvider { * * @param id Page ID. * @param wikiId Wiki ID. - * @param name Name of the wiki. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - logPageView(id: number, wikiId: number, name?: string, siteId?: string): Promise { + logPageView(id: number, wikiId: number, siteId?: string): Promise { const params: AddonModWikiViewPageWSParams = { pageid: id, }; - return CoreCourseLogHelper.logSingle( + return CoreCourseLogHelper.log( 'mod_wiki_view_page', params, AddonModWikiProvider.COMPONENT, wikiId, - name, - 'wiki', - params, siteId, ); } @@ -646,23 +642,19 @@ export class AddonModWikiProvider { * Report the wiki as being viewed. * * @param id Wiki ID. - * @param name Name of the wiki. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - logView(id: number, name?: string, siteId?: string): Promise { + logView(id: number, siteId?: string): Promise { const params: AddonModWikiViewWikiWSParams = { wikiid: id, }; - return CoreCourseLogHelper.logSingle( + return CoreCourseLogHelper.log( 'mod_wiki_view_wiki', params, AddonModWikiProvider.COMPONENT, id, - name, - 'wiki', - {}, siteId, ); } diff --git a/src/addons/mod/workshop/components/index/index.ts b/src/addons/mod/workshop/components/index/index.ts index cfed3a329..8b7c54776 100644 --- a/src/addons/mod/workshop/components/index/index.ts +++ b/src/addons/mod/workshop/components/index/index.ts @@ -66,7 +66,7 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity @Input() group = 0; component = AddonModWorkshopProvider.COMPONENT; - moduleName = 'workshop'; + pluginName = 'workshop'; workshop?: AddonModWorkshopData; page = 0; @@ -255,7 +255,9 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity return; // Shouldn't happen. } - await AddonModWorkshop.logView(this.workshop.id, this.workshop.name); + await CoreUtils.ignoreErrors(AddonModWorkshop.logView(this.workshop.id)); + + this.analyticsLogEvent('mod_workshop_view_workshop'); } /** diff --git a/src/addons/mod/workshop/pages/assessment/assessment.ts b/src/addons/mod/workshop/pages/assessment/assessment.ts index 961591bac..973a16a03 100644 --- a/src/addons/mod/workshop/pages/assessment/assessment.ts +++ b/src/addons/mod/workshop/pages/assessment/assessment.ts @@ -39,6 +39,8 @@ import { import { AddonModWorkshopHelper, AddonModWorkshopSubmissionAssessmentWithFormData } from '../../services/workshop-helper'; import { AddonModWorkshopOffline } from '../../services/workshop-offline'; import { AddonModWorkshopSyncProvider } from '../../services/workshop-sync'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays a workshop assessment. @@ -89,6 +91,7 @@ export class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy, CanLea protected siteId: string; protected currentUserId: number; protected forceLeave = false; + protected logView: () => void; constructor( protected fb: FormBuilder, @@ -111,6 +114,20 @@ export class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy, CanLea this.refreshAllData(); } }, this.siteId); + + this.logView = CoreTime.once(async () => { + if (!this.workshop) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_workshop_get_assessment', + name: this.workshop.name, + data: { id: this.workshop.id, assessmentid: this.assessment.id, category: 'workshop' }, + url: `/mod/workshop/assessment.php?asid=${this.assessment.id}`, + }); + }); } /** diff --git a/src/addons/mod/workshop/pages/edit-submission/edit-submission.ts b/src/addons/mod/workshop/pages/edit-submission/edit-submission.ts index d83fd9d4c..4c7ea2d08 100644 --- a/src/addons/mod/workshop/pages/edit-submission/edit-submission.ts +++ b/src/addons/mod/workshop/pages/edit-submission/edit-submission.ts @@ -40,6 +40,7 @@ import { } from '../../services/workshop'; import { AddonModWorkshopHelper, AddonModWorkshopSubmissionDataWithOfflineData } from '../../services/workshop-helper'; import { AddonModWorkshopOffline } from '../../services/workshop-offline'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays the workshop edit submission. @@ -224,6 +225,8 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy, Ca } this.loaded = true; + + this.logView(); } catch (error) { this.loaded = false; @@ -233,6 +236,23 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy, Ca } } + /** + * Log view. + */ + protected logView(): void { + if (!this.workshop) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: this.editing ? 'mod_workshop_update_submission' : 'mod_workshop_add_submission', + name: this.workshop.name, + data: { id: this.workshop.id, submissionid: this.submissionId, category: 'workshop' }, + url: `/mod/workshop/submission.php?cmid=${this.module.id}&id=${this.submissionId}&edit=on`, + }); + } + /** * Force leaving the page, without checking for changes. */ diff --git a/src/addons/mod/workshop/pages/submission/submission.ts b/src/addons/mod/workshop/pages/submission/submission.ts index 711c20fb5..faf95933b 100644 --- a/src/addons/mod/workshop/pages/submission/submission.ts +++ b/src/addons/mod/workshop/pages/submission/submission.ts @@ -47,6 +47,8 @@ import { } from '../../services/workshop-helper'; import { AddonModWorkshopOffline } from '../../services/workshop-offline'; import { AddonModWorkshopSyncProvider, AddonModWorkshopAutoSyncData } from '../../services/workshop-sync'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreTime } from '@singletons/time'; /** * Page that displays a workshop submission. @@ -102,7 +104,7 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLea protected obsAssessmentSaved: CoreEventObserver; protected syncObserver: CoreEventObserver; protected isDestroyed = false; - protected fetchSuccess = false; + protected logView: () => void; constructor( protected fb: FormBuilder, @@ -125,6 +127,8 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLea // Update just when all database is synced. this.eventReceived(data); }, this.siteId); + + this.logView = CoreTime.once(() => this.performLogView()); } /** @@ -599,19 +603,21 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLea /** * Log submission viewed. */ - protected async logView(): Promise { - if (this.fetchSuccess) { - return; // Already done. - } - - this.fetchSuccess = true; - + protected async performLogView(): Promise { try { - await AddonModWorkshop.logViewSubmission(this.submissionId, this.workshopId, this.workshop.name); + await AddonModWorkshop.logViewSubmission(this.submissionId, this.workshopId); CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata); } catch { // Ignore errors. } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_workshop_view_submission', + name: this.workshop.name, + data: { id: this.workshop.id, submissionid: this.submissionId, category: 'workshop' }, + url: `/mod/workshop/submission.php?cmid=${this.module.id}&id=${this.submissionId}`, + }); } /** diff --git a/src/addons/mod/workshop/services/workshop.ts b/src/addons/mod/workshop/services/workshop.ts index 26c14809d..a70f23a55 100644 --- a/src/addons/mod/workshop/services/workshop.ts +++ b/src/addons/mod/workshop/services/workshop.ts @@ -1443,23 +1443,19 @@ export class AddonModWorkshopProvider { * Report the workshop as being viewed. * * @param id Workshop ID. - * @param name Name of the workshop. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - async logView(id: number, name?: string, siteId?: string): Promise { + async logView(id: number, siteId?: string): Promise { const params: AddonModWorkshopViewWorkshopWSParams = { workshopid: id, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_workshop_view_workshop', params, AddonModWorkshopProvider.COMPONENT, id, - name, - 'workshop', - {}, siteId, ); } @@ -1469,23 +1465,19 @@ export class AddonModWorkshopProvider { * * @param id Submission ID. * @param workshopId Workshop ID. - * @param name Name of the workshop. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - async logViewSubmission(id: number, workshopId: number, name?: string, siteId?: string): Promise { + async logViewSubmission(id: number, workshopId: number, siteId?: string): Promise { const params: AddonModWorkshopViewSubmissionWSParams = { submissionid: id, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_workshop_view_submission', params, AddonModWorkshopProvider.COMPONENT, workshopId, - name, - 'workshop', - params, siteId, ); } diff --git a/src/addons/notes/pages/list/list.ts b/src/addons/notes/pages/list/list.ts index 1369ec308..58bf0e68e 100644 --- a/src/addons/notes/pages/list/list.ts +++ b/src/addons/notes/pages/list/list.ts @@ -21,12 +21,16 @@ import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { CoreAnimations } from '@components/animations'; import { CoreUser, CoreUserProfile } from '@features/user/services/user'; import { IonContent, IonRefresher } from '@ionic/angular'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreDomUtils, ToastDuration } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; +import { CoreUrlUtils } from '@services/utils/url'; import { CoreUtils } from '@services/utils/utils'; +import { Translate } from '@singletons'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; +import { CoreTime } from '@singletons/time'; /** * Page that displays a list of notes. @@ -54,9 +58,11 @@ export class AddonNotesListPage implements OnInit, OnDestroy { currentUserId!: number; protected syncObserver!: CoreEventObserver; - protected logAfterFetch = true; + protected logView: () => void; constructor() { + this.logView = CoreTime.once(() => this.performLogView()); + try { this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); this.userId = CoreNavigator.getRouteNumberParam('userId'); @@ -128,10 +134,7 @@ export class AddonNotesListPage implements OnInit, OnDestroy { this.notes = await AddonNotes.getNotesUserData(notesList); } - if (this.logAfterFetch) { - this.logAfterFetch = false; - CoreUtils.ignoreErrors(AddonNotes.logView(this.courseId, this.userId)); - } + this.logView(); } catch (error) { CoreDomUtils.showErrorModal(error); } finally { @@ -176,7 +179,6 @@ export class AddonNotesListPage implements OnInit, OnDestroy { this.notesLoaded = false; this.refreshIcon = CoreConstants.ICON_LOADING; this.syncIcon = CoreConstants.ICON_LOADING; - this.logAfterFetch = true; await this.fetchNotes(true); } @@ -190,6 +192,8 @@ export class AddonNotesListPage implements OnInit, OnDestroy { e.preventDefault(); e.stopPropagation(); + this.logViewAdd(); + const modalData = await CoreDomUtils.openModal({ component: AddonNotesAddComponent, componentProps: { @@ -225,6 +229,8 @@ export class AddonNotesListPage implements OnInit, OnDestroy { e.stopPropagation(); try { + this.logViewDelete(note); + await CoreDomUtils.showDeleteConfirm('addon.notes.deleteconfirm'); try { await AddonNotes.deleteNote(note, this.courseId); @@ -294,6 +300,58 @@ export class AddonNotesListPage implements OnInit, OnDestroy { } } + /** + * Log view. + */ + protected async performLogView(): Promise { + await CoreUtils.ignoreErrors(AddonNotes.logView(this.courseId, this.userId)); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_notes_view_notes', + name: Translate.instant('addon.notes.notes'), + data: { courseid: this.courseId, userid: this.userId || 0, category: 'notes' }, + url: CoreUrlUtils.addParamsToUrl('/notes/index.php', { + user: this.userId, + course: this.courseId !== CoreSites.getCurrentSiteHomeId() ? this.courseId : undefined, + }), + }); + } + + /** + * Log view. + */ + protected async logViewAdd(): Promise { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'core_notes_create_notes', + name: Translate.instant('addon.notes.notes'), + data: { courseid: this.courseId, userid: this.userId || 0, category: 'notes' }, + url: CoreUrlUtils.addParamsToUrl('/notes/edit.php', { + courseid: this.courseId, + userid: this.userId, + publishstate: this.type === 'personal' ? 'draft' : (this.type === 'course' ? 'public' : 'site'), + }), + }); + } + + /** + * Log view. + */ + protected async logViewDelete(note: AddonNotesNoteFormatted): Promise { + if (!note.id) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'core_notes_delete_notes', + name: Translate.instant('addon.notes.notes'), + data: { id: note.id, category: 'notes' }, + url: `/notes/delete.php?id=${note.id}`, + }); + } + /** * Page destroyed. */ diff --git a/src/addons/notes/services/notes.ts b/src/addons/notes/services/notes.ts index 71e4d3c06..0c2cfd8db 100644 --- a/src/addons/notes/services/notes.ts +++ b/src/addons/notes/services/notes.ts @@ -15,7 +15,6 @@ import { Injectable } from '@angular/core'; import { CoreWSError } from '@classes/errors/wserror'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; -import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; import { CoreUser } from '@features/user/services/user'; import { CoreNetwork } from '@services/network'; import { CoreSites } from '@services/sites'; @@ -414,8 +413,6 @@ export class AddonNotesProvider { userid: userId || 0, }; - CorePushNotifications.logViewListEvent('notes', 'core_notes_view_notes', params, site.getId()); - await site.write('core_notes_view_notes', params); } diff --git a/src/addons/notifications/pages/notification/notification.ts b/src/addons/notifications/pages/notification/notification.ts index 7a5b4edb8..a8f6258df 100644 --- a/src/addons/notifications/pages/notification/notification.ts +++ b/src/addons/notifications/pages/notification/notification.ts @@ -24,9 +24,11 @@ import { ActivatedRouteSnapshot } from '@angular/router'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; import { CoreContentLinksAction, CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; +import { Translate } from '@singletons'; /** * Page to render a notification. @@ -72,6 +74,16 @@ export class AddonNotificationsNotificationPage implements OnInit, OnDestroy { AddonNotificationsHelper.markNotificationAsRead(notification); this.loaded = true; + + if (notification.id) { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'core_message_get_messages', + name: Translate.instant('addon.notifications.notifications'), + data: { id: notification.id, category: 'notifications' }, + url: `/message/output/popup/notifications.php?notificationid=${notification.id}&offset=0`, + }); + } } /** diff --git a/src/addons/notifications/pages/settings/settings.ts b/src/addons/notifications/pages/settings/settings.ts index b41f96611..f0c7a5d56 100644 --- a/src/addons/notifications/pages/settings/settings.ts +++ b/src/addons/notifications/pages/settings/settings.ts @@ -37,6 +37,9 @@ import { AddonNotificationsPreferencesProcessorFormatted, } from '@addons/notifications/services/notifications-helper'; import { CoreNavigator } from '@services/navigator'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { Translate } from '@singletons'; /** * Page that displays notifications settings. @@ -58,12 +61,23 @@ export class AddonNotificationsSettingsPage implements OnInit, OnDestroy { loggedInOffLegacyMode = false; protected updateTimeout?: number; + protected logView: () => void; constructor() { this.canChangeSound = CoreLocalNotifications.canDisableSound(); const currentSite = CoreSites.getRequiredCurrentSite(); this.loggedInOffLegacyMode = !currentSite.isVersionGreaterEqualThan('4.0'); + + this.logView = CoreTime.once(async () => { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_message_get_user_notification_preferences', + name: Translate.instant('addon.notifications.notificationpreferences'), + data: { category: 'notifications' }, + url: '/message/notificationpreferences.php', + }); + }); } /** @@ -100,6 +114,8 @@ export class AddonNotificationsSettingsPage implements OnInit, OnDestroy { preferences.enableall = !preferences.disableall; this.preferences = AddonNotificationsHelper.formatPreferences(preferences); this.loadProcessor(currentProcessor); + + this.logView(); } catch (error) { CoreDomUtils.showErrorModal(error); } finally { diff --git a/src/addons/privatefiles/pages/index/index.ts b/src/addons/privatefiles/pages/index/index.ts index 6cf24b9f7..4eca6fd5f 100644 --- a/src/addons/privatefiles/pages/index/index.ts +++ b/src/addons/privatefiles/pages/index/index.ts @@ -32,6 +32,8 @@ import { import { AddonPrivateFilesHelper } from '@addons/privatefiles/services/privatefiles-helper'; import { CoreUtils } from '@services/utils/utils'; import { CoreNavigator } from '@services/navigator'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays the list of files. @@ -57,12 +59,23 @@ export class AddonPrivateFilesIndexPage implements OnInit, OnDestroy { filesLoaded = false; // Whether the files are loaded. protected updateSiteObserver: CoreEventObserver; + protected logView: () => void; constructor() { // Update visibility if current site info is updated. this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => { this.setVisibility(); }, CoreSites.getCurrentSiteId()); + + this.logView = CoreTime.once(async () => { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_files_get_files', + name: Translate.instant('addon.privatefiles.files'), + data: { category: 'files' }, + url: '/user/files.php', + }); + }); } /** @@ -208,6 +221,8 @@ export class AddonPrivateFilesIndexPage implements OnInit, OnDestroy { // User quota isn't useful, delete it. delete this.userQuota; } + + this.logView(); } else { // Unknown root. CoreDomUtils.showErrorModal('addon.privatefiles.couldnotloadfiles', true); diff --git a/src/core/classes/items-management/list-items-manager.ts b/src/core/classes/items-management/list-items-manager.ts index 83c466882..3ec31d484 100644 --- a/src/core/classes/items-management/list-items-manager.ts +++ b/src/core/classes/items-management/list-items-manager.ts @@ -23,6 +23,7 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreRoutedItemsManagerSource } from './routed-items-manager-source'; import { CoreRoutedItemsManager } from './routed-items-manager'; import { CoreDom } from '@singletons/dom'; +import { CoreTime } from '@singletons/time'; /** * Helper class to manage the state and routing of a list of items in a page. @@ -35,7 +36,7 @@ export class CoreListItemsManager< protected pageRouteLocator?: unknown | ActivatedRoute; protected splitView?: CoreSplitViewComponent; protected splitViewOutletSubscription?: Subscription; - protected fetchSuccess = false; // Whether a fetch was finished successfully. + protected finishSuccessfulFetch: () => void; constructor(source: Source, pageRouteLocator: unknown | ActivatedRoute) { super(source); @@ -44,6 +45,7 @@ export class CoreListItemsManager< this.pageRouteLocator = pageRouteLocator; this.addListener({ onSelectedItemUpdated: debouncedScrollToCurrentElement }); + this.finishSuccessfulFetch = CoreTime.once(() => CoreUtils.ignoreErrors(this.logActivity())); } get items(): Item[] { @@ -160,19 +162,6 @@ export class CoreListItemsManager< this.finishSuccessfulFetch(); } - /** - * Finish a successful fetch. - */ - protected async finishSuccessfulFetch(): Promise { - if (this.fetchSuccess) { - return; // Already treated. - } - - // Log activity. - this.fetchSuccess = true; - await CoreUtils.ignoreErrors(this.logActivity()); - } - /** * Log activity when the page starts. */ diff --git a/src/core/features/course/classes/main-activity-component.ts b/src/core/features/course/classes/main-activity-component.ts index e2a8d20fd..02f2e8127 100644 --- a/src/core/features/course/classes/main-activity-component.ts +++ b/src/core/features/course/classes/main-activity-component.ts @@ -34,7 +34,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR @Input() group?: number; // Group ID the component belongs to. - moduleName?: string; // Raw module name to be translated. It will be translated on init. + moduleName = ''; // Translated module name. Calculated from pluginName. protected syncObserver?: CoreEventObserver; // It will observe the sync auto event. protected syncEventName?: string; // Auto sync event name. @@ -54,7 +54,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR await super.ngOnInit(); this.hasOffline = false; - this.moduleName = CoreCourse.translateModuleName(this.moduleName || ''); + this.moduleName = CoreCourse.translateModuleName(this.pluginName || this.moduleName || ''); if (this.syncEventName) { // Refresh data if this discussion is synchronized automatically. diff --git a/src/core/features/course/classes/main-resource-component.ts b/src/core/features/course/classes/main-resource-component.ts index 5944a2a3b..0e3761b1e 100644 --- a/src/core/features/course/classes/main-resource-component.ts +++ b/src/core/features/course/classes/main-resource-component.ts @@ -31,6 +31,9 @@ import { CoreCourse } from '../services/course'; import { CoreCourseHelper, CoreCourseModuleData } from '../services/course-helper'; import { CoreCourseModuleDelegate, CoreCourseModuleMainComponent } from '../services/module-delegate'; import { CoreCourseModulePrefetchDelegate } from '../services/module-prefetch-delegate'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreUrlUtils } from '@services/utils/url'; +import { CoreTime } from '@singletons/time'; /** * Result of a resource download. @@ -56,8 +59,8 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, component?: string; // Component name. componentId?: number; // Component ID. hasOffline = false; // Resources don't have any data to sync. - description?: string; // Module description. + pluginName?: string; // The plugin name without "mod_", e.g. assign or book. protected fetchContentDefaultError = 'core.course.errorgetmodule'; // Default error to show when loading contents. protected isCurrentView = false; // Whether the component is in the current view. @@ -72,14 +75,16 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, protected showCompletion = false; // Whether to show completion inside the activity. protected displayDescription = true; // Wether to show Module description on module page, and not on summary or the contrary. protected isDestroyed = false; // Whether the component is destroyed. - protected fetchSuccess = false; // Whether a fetch was finished successfully. protected checkCompletionAfterLog = true; // Whether to check if completion has changed after calling logActivity. + protected finishSuccessfulFetch: () => void; constructor( @Optional() @Inject('') loggerName: string = 'CoreCourseModuleMainResourceComponent', protected courseContentsPage?: CoreCourseContentsPage, ) { this.logger = CoreLogger.getInstance(loggerName); + + this.finishSuccessfulFetch = CoreTime.once(() => this.performFinishSuccessfulFetch()); } /** @@ -447,16 +452,11 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, } /** - * Finish a successful fetch. + * Finish first successful fetch. * * @returns Promise resolved when done. */ - protected async finishSuccessfulFetch(): Promise { - if (this.fetchSuccess) { - return; // Already treated. - } - - this.fetchSuccess = true; + protected async performFinishSuccessfulFetch(): Promise { this.storeModuleViewed(); // Log activity now. @@ -489,6 +489,36 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, // To be overridden. } + /** + * Log activity view in analytics. + * + * @param wsName Name of the WS used. + * @param data Other data to send. + * @returns Promise resolved when done. + */ + async analyticsLogEvent( + wsName: string, + options: AnalyticsLogEventOptions = {}, + ): Promise { + let url: string | undefined; + if (options.sendUrl === true || options.sendUrl === undefined) { + if (typeof options.url === 'string') { + url = options.url; + } else if (this.pluginName) { + // Use default value. + url = CoreUrlUtils.addParamsToUrl(`/mod/${this.pluginName}/view.php?id=${this.module.id}`, options.data); + } + } + + await CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: wsName, + name: options.name || this.module.name, + data: { id: this.module.instance, category: this.pluginName, ...options.data }, + url, + }); + } + /** * Check the module completion. */ @@ -534,3 +564,10 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, } } + +type AnalyticsLogEventOptions = { + data?: Record; // Other data to send. + name?: string; // Name to send, defaults to activity name. + url?: string; // URL to use. If not set and sendUrl is true, a default value will be used. + sendUrl?: boolean; // Whether to pass a URL to analytics. Defaults to true. +}; diff --git a/src/core/features/course/components/course-format/course-format.ts b/src/core/features/course/components/course-format/course-format.ts index 697cdd331..381c6040a 100644 --- a/src/core/features/course/components/course-format/course-format.ts +++ b/src/core/features/course/components/course-format/course-format.ts @@ -50,6 +50,7 @@ import { CoreUserToursAlignment, CoreUserToursSide } from '@features/usertours/s import { CoreCourseCourseIndexTourComponent } from '../course-index-tour/course-index-tour'; import { CoreDom } from '@singletons/dom'; import { CoreUserTourDirectiveOptions } from '@directives/user-tour'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Component to display course contents using a certain format. If the format isn't found, use default one. @@ -518,9 +519,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { this.content.scrollToTop(0); } - CoreUtils.ignoreErrors( - CoreCourse.logView(this.course.id, newSection.section, undefined, this.course.fullname), - ); + this.logView(newSection.section, !previousValue); } this.changeDetectorRef.markForCheck(); } @@ -655,6 +654,37 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { return CoreCourseHelper.canUserViewSection(section) && !CoreCourseHelper.isSectionStealth(section); } + /** + * Log view. + * + * @param sectionNumber Section loaded (if any). + * @param firstLoad Whether it's the first load when opening the course. + */ + async logView(sectionNumber?: number, firstLoad = false): Promise { + await CoreUtils.ignoreErrors( + CoreCourse.logView(this.course.id, sectionNumber), + ); + + let extraParams = sectionNumber ? `§ion=${sectionNumber}` : ''; + if (firstLoad && sectionNumber) { + // If course is configured to show all sections in one page, don't include section in URL in first load. + const courseDisplay = 'courseformatoptions' in this.course && + this.course.courseformatoptions?.find(option => option.name === 'coursedisplay'); + + if (!courseDisplay || Number(courseDisplay.value) !== 0) { + extraParams = ''; + } + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'core_course_view_course', + name: this.course.fullname, + data: { id: this.course.id, sectionnumber: sectionNumber, category: 'course' }, + url: `/course/view.php?id=${this.course.id}${extraParams}`, + }); + } + } type CoreCourseSectionToDisplay = CoreCourseSection & { diff --git a/src/core/features/course/pages/course-summary/course-summary.page.ts b/src/core/features/course/pages/course-summary/course-summary.page.ts index b510fc6b3..8185b5989 100644 --- a/src/core/features/course/pages/course-summary/course-summary.page.ts +++ b/src/core/features/course/pages/course-summary/course-summary.page.ts @@ -41,6 +41,8 @@ import { CorePromisedValue } from '@classes/promised-value'; import { CorePlatform } from '@services/platform'; import { CoreCourse } from '@features/course/services/course'; import { CorePasswordModalResponse } from '@components/password-modal/password-modal'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; const ENROL_BROWSER_METHODS = ['fee', 'paypal']; @@ -81,6 +83,7 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy { protected courseStatusObserver?: CoreEventObserver; protected appResumeSubscription: Subscription; protected waitingForBrowserEnrol = false; + protected logView: () => void; constructor() { // Refresh the view when the app is resumed. @@ -96,6 +99,20 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy { await this.refreshData(); }); }); + + this.logView = CoreTime.once(async () => { + if (!this.course || this.isModal) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'core_course_get_courses', + name: this.course.fullname, + data: { id: this.course.id, category: 'course' }, + url: `/enrol/index.php?id=${this.course.id}`, + }); + }); } /** @@ -161,6 +178,8 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy { this.getCourseData(), this.loadCourseExtraData(), ]); + + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error getting enrolment data'); } diff --git a/src/core/features/course/pages/list-mod-type/list-mod-type.ts b/src/core/features/course/pages/list-mod-type/list-mod-type.ts index e0eb86ae3..c27314dcc 100644 --- a/src/core/features/course/pages/list-mod-type/list-mod-type.ts +++ b/src/core/features/course/pages/list-mod-type/list-mod-type.ts @@ -22,6 +22,8 @@ import { CoreNavigator } from '@services/navigator'; import { CoreConstants } from '@/core/constants'; import { IonRefresher } from '@ionic/angular'; import { CoreUtils } from '@services/utils/utils'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays all modules of a certain type in a course. @@ -39,6 +41,24 @@ export class CoreCourseListModTypePage implements OnInit { protected modName?: string; protected archetypes: Record = {}; // To speed up the check of modules. + protected logView: () => void; + + constructor() { + this.logView = CoreTime.once(async () => { + if (!this.modName) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_course_get_contents', + name: this.title, + data: { category: this.modName }, + url: (this.modName === 'resources' ? '/course/resources.php' : `/mod/${this.modName}/index.php`) + + `?id=${this.courseId}`, + }); + }); + } /** * @inheritdoc diff --git a/src/core/features/course/services/course.ts b/src/core/features/course/services/course.ts index f7506171e..6788eb699 100644 --- a/src/core/features/course/services/course.ts +++ b/src/core/features/course/services/course.ts @@ -38,7 +38,6 @@ import { } from '../../courses/services/courses'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreWSError } from '@classes/errors/wserror'; -import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; import { CoreCourseHelper, CoreCourseModuleData, CoreCourseModuleCompletionData } from './course-helper'; import { CoreCourseFormatDelegate } from './format-delegate'; import { CoreCronDelegate } from '@services/cron'; @@ -1191,22 +1190,19 @@ export class CoreCourseProvider { * @param courseId Course ID. * @param sectionNumber Section number. * @param siteId Site ID. If not defined, current site. - * @param name Name of the course. * @returns Promise resolved when the WS call is successful. */ - async logView(courseId: number, sectionNumber?: number, siteId?: string, name?: string): Promise { + async logView(courseId: number, sectionNumber?: number, siteId?: string): Promise { const params: CoreCourseViewCourseWSParams = { courseid: courseId, }; - const wsName = 'core_course_view_course'; if (sectionNumber !== undefined) { params.sectionnumber = sectionNumber; } const site = await CoreSites.getSite(siteId); - CorePushNotifications.logViewEvent(courseId, name, 'course', wsName, { sectionnumber: sectionNumber }, siteId); - const response: CoreStatusWithWarningsWSResponse = await site.write(wsName, params); + const response: CoreStatusWithWarningsWSResponse = await site.write('core_course_view_course', params); if (!response.status) { throw Error('WS core_course_view_course failed.'); diff --git a/src/core/features/course/services/handlers/log-cron.ts b/src/core/features/course/services/handlers/log-cron.ts index fe87cd189..f9653eeeb 100644 --- a/src/core/features/course/services/handlers/log-cron.ts +++ b/src/core/features/course/services/handlers/log-cron.ts @@ -44,7 +44,7 @@ export class CoreCourseLogCronHandlerService implements CoreCronHandler { const site = await CoreSites.getSite(siteId); - return CoreCourse.logView(site.getSiteHomeId(), undefined, site.getId(), site.getInfo()?.sitename); + return CoreCourse.logView(site.getSiteHomeId(), undefined, site.getId()); } /** diff --git a/src/core/features/courses/pages/categories/categories.ts b/src/core/features/courses/pages/categories/categories.ts index 903c19012..f081616d8 100644 --- a/src/core/features/courses/pages/categories/categories.ts +++ b/src/core/features/courses/pages/categories/categories.ts @@ -21,6 +21,8 @@ import { CoreCategoryData, CoreCourseListItem, CoreCourses, CoreCoursesProvider import { Translate } from '@singletons'; import { CoreNavigator } from '@services/navigator'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreTime } from '@singletons/time'; /** * Page that displays a list of categories and the courses in the current category if any. @@ -50,6 +52,7 @@ export class CoreCoursesCategoriesPage implements OnInit, OnDestroy { protected siteUpdatedObserver: CoreEventObserver; protected downloadEnabledObserver: CoreEventObserver; protected isDestroyed = false; + protected logView: () => void; constructor() { this.title = Translate.instant('core.courses.categories'); @@ -78,6 +81,16 @@ export class CoreCoursesCategoriesPage implements OnInit, OnDestroy { this.downloadEnabledObserver = CoreEvents.on(CoreCoursesProvider.EVENT_DASHBOARD_DOWNLOAD_ENABLED_CHANGED, (data) => { this.downloadEnabled = (this.downloadCourseEnabled || this.downloadCoursesEnabled) && data.enabled; }); + + this.logView = CoreTime.once(() => { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_course_get_categories', + name: this.title, + data: { categoryid: this.categoryId, category: 'course' }, + url: '/course/index.php' + (this.categoryId > 0 ? `?categoryid=${this.categoryId}` : ''), + }); + }); } /** @@ -138,6 +151,8 @@ export class CoreCoursesCategoriesPage implements OnInit, OnDestroy { !this.isDestroyed && CoreDomUtils.showErrorModalDefault(error, 'core.courses.errorloadcourses', true); } } + + this.logView(); } catch (error) { !this.isDestroyed && CoreDomUtils.showErrorModalDefault(error, 'core.courses.errorloadcategories', true); } diff --git a/src/core/features/courses/pages/dashboard/dashboard.ts b/src/core/features/courses/pages/dashboard/dashboard.ts index 8bca938e0..cc0ad8382 100644 --- a/src/core/features/courses/pages/dashboard/dashboard.ts +++ b/src/core/features/courses/pages/dashboard/dashboard.ts @@ -24,6 +24,9 @@ import { CoreCourseBlock } from '@features/course/services/course'; import { CoreBlockComponent } from '@features/block/components/block/block'; import { CoreNavigator } from '@services/navigator'; import { CoreBlockDelegate } from '@features/block/services/block-delegate'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { Translate } from '@singletons'; /** * Page that displays the dashboard page. @@ -46,6 +49,7 @@ export class CoreCoursesDashboardPage implements OnInit, OnDestroy { loaded = false; protected updateSiteObserver: CoreEventObserver; + protected logView: () => void; constructor() { // Refresh the enabled flags if site is updated. @@ -55,6 +59,16 @@ export class CoreCoursesDashboardPage implements OnInit, OnDestroy { this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite(); }, CoreSites.getCurrentSiteId()); + + this.logView = CoreTime.once(async () => { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_my_view_page', + name: Translate.instant('core.courses.mymoodle'), + data: { category: 'course' }, + url: '/my/', + }); + }); } /** @@ -102,6 +116,8 @@ export class CoreCoursesDashboardPage implements OnInit, OnDestroy { } this.loaded = true; + + this.logView(); } /** diff --git a/src/core/features/courses/pages/list/list.ts b/src/core/features/courses/pages/list/list.ts index 3ba71b126..7e754f9ea 100644 --- a/src/core/features/courses/pages/list/list.ts +++ b/src/core/features/courses/pages/list/list.ts @@ -20,6 +20,9 @@ import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreCourseBasicSearchedData, CoreCourses, CoreCoursesProvider } from '../../services/courses'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { Translate } from '@singletons'; type CoreCoursesListMode = 'search' | 'all' | 'my'; @@ -61,6 +64,7 @@ export class CoreCoursesListPage implements OnInit, OnDestroy { protected downloadEnabledObserver: CoreEventObserver; protected courseIds = ''; protected isDestroyed = false; + protected logView: () => void; constructor() { this.currentSiteId = CoreSites.getRequiredCurrentSite().getId(); @@ -96,6 +100,26 @@ export class CoreCoursesListPage implements OnInit, OnDestroy { this.downloadEnabledObserver = CoreEvents.on(CoreCoursesProvider.EVENT_DASHBOARD_DOWNLOAD_ENABLED_CHANGED, (data) => { this.downloadEnabled = (this.downloadCourseEnabled || this.downloadCoursesEnabled) && data.enabled; }); + + this.logView = CoreTime.once(async () => { + if (this.showOnlyEnrolled) { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_enrol_get_users_courses', + name: Translate.instant('core.courses.mycourses'), + data: { category: 'course' }, + url: '/my/courses.php', + }); + } else { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_course_get_courses_by_field', + name: Translate.instant('core.courses.availablecourses'), + data: { category: 'course' }, + url: '/course/index.php', + }); + } + }); } /** @@ -176,6 +200,8 @@ export class CoreCoursesListPage implements OnInit, OnDestroy { this.coursesLoaded = this.courses.length; this.canLoadMore = this.loadedCourses.length > this.courses.length; + + this.logView(); } catch (error) { this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading. !this.isDestroyed && CoreDomUtils.showErrorModalDefault(error, 'core.courses.errorloadcourses', true); diff --git a/src/core/features/courses/pages/my/my.ts b/src/core/features/courses/pages/my/my.ts index c9d956500..3f9ab7907 100644 --- a/src/core/features/courses/pages/my/my.ts +++ b/src/core/features/courses/pages/my/my.ts @@ -29,6 +29,9 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { Subscription } from 'rxjs'; import { CoreCourses } from '../../services/courses'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { Translate } from '@singletons'; /** * Page that shows a my courses. @@ -58,6 +61,7 @@ export class CoreCoursesMyPage implements OnInit, OnDestroy, AsyncDirective { protected updateSiteObserver: CoreEventObserver; protected onReadyPromise = new CorePromisedValue(); protected loadsManagerSubscription: Subscription; + protected logView: () => void; constructor(protected loadsManager: PageLoadsManager) { // Refresh the enabled flags if site is updated. @@ -73,6 +77,16 @@ export class CoreCoursesMyPage implements OnInit, OnDestroy, AsyncDirective { this.loaded = false; this.loadContent(); }); + + this.logView = CoreTime.once(async () => { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_enrol_get_users_courses', + name: Translate.instant('core.courses.mycourses'), + data: { category: 'course' }, + url: '/my/courses.php', + }); + }); } /** @@ -138,6 +152,8 @@ export class CoreCoursesMyPage implements OnInit, OnDestroy, AsyncDirective { this.loaded = true; this.onReadyPromise.resolve(); + + this.logView(); } /** diff --git a/src/core/features/grades/pages/course/course.page.ts b/src/core/features/grades/pages/course/course.page.ts index 7bca24a87..d148992d1 100644 --- a/src/core/features/grades/pages/course/course.page.ts +++ b/src/core/features/grades/pages/course/course.page.ts @@ -35,6 +35,8 @@ import { CoreUserParticipantsSource } from '@features/user/classes/participants- import { CoreUserData, CoreUserParticipant } from '@features/user/services/user'; import { CoreGradesCoursesSource } from '@features/grades/classes/grades-courses-source'; import { CoreDom } from '@singletons/dom'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays a course grades. @@ -61,12 +63,14 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy { loaded = false; protected useLegacyLayout?: boolean; // Whether to use the layout before 4.1. - protected fetchSuccess = false; + protected logView: () => void; constructor( protected route: ActivatedRoute, protected element: ElementRef, ) { + this.logView = CoreTime.once(() => this.performLogView()); + try { this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId', { route }); this.userId = CoreNavigator.getRouteNumberParam('userId', { route }) ?? CoreSites.getCurrentSiteUserId(); @@ -232,10 +236,7 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy { this.rowsOnView = this.getRowsOnHeight(); this.totalColumnsSpan = formattedTable.columns.reduce((total, column) => total + column.colspan, 0); - if (!this.fetchSuccess) { - this.fetchSuccess = true; - await CoreGrades.logCourseGradesView(this.courseId, this.userId); - } + this.logView(); } /** @@ -257,6 +258,22 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy { infiniteComplete && infiniteComplete(); } + /** + * Log view. + */ + protected async performLogView(): Promise { + await CoreUtils.ignoreErrors(CoreGrades.logCourseGradesView(this.courseId, this.userId)); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'gradereport_user_view_grade_report', + name: this.title ?? '', + data: { id: this.courseId, userid: this.userId, category: 'grades' }, + url: `/grade/report/user/index.php?id=${this.courseId}` + + (this.userId !== CoreSites.getCurrentSiteUserId() ? `&userid=${this.userId}` : ''), + }); + } + } /** diff --git a/src/core/features/grades/pages/courses/courses.ts b/src/core/features/grades/pages/courses/courses.ts index dd47d084c..79e55d35a 100644 --- a/src/core/features/grades/pages/courses/courses.ts +++ b/src/core/features/grades/pages/courses/courses.ts @@ -20,8 +20,11 @@ import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreGradesCoursesSource } from '@features/grades/classes/grades-courses-source'; import { CoreGrades } from '@features/grades/services/grades'; import { IonRefresher } from '@ionic/angular'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; +import { Translate } from '@singletons'; /** * Page that displays courses grades (main menu option). @@ -93,6 +96,14 @@ class CoreGradesCoursesManager extends CoreListItemsManager { */ protected async logActivity(): Promise { await CoreGrades.logCoursesGradesView(); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'gradereport_overview_view_grade_report', + name: Translate.instant('core.grades.grades'), + data: { courseId: CoreSites.getCurrentSiteHomeId(), category: 'grades' }, + url: '/grade/report/overview/index.php', + }); } } diff --git a/src/core/features/grades/services/grades.ts b/src/core/features/grades/services/grades.ts index be80ee574..85ae19c4e 100644 --- a/src/core/features/grades/services/grades.ts +++ b/src/core/features/grades/services/grades.ts @@ -15,7 +15,6 @@ import { Injectable } from '@angular/core'; import { CoreCourses } from '@features/courses/services/courses'; import { CoreSites } from '@services/sites'; -import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; import { makeSingleton } from '@singletons'; import { CoreLogger } from '@singletons/logger'; import { CoreWSExternalWarning } from '@services/ws'; @@ -358,34 +357,16 @@ export class CoreGradesProvider { * * @param courseId Course ID. * @param userId User ID. - * @param name Course name. If not set, it will be calculated. * @returns Promise resolved when done. */ - async logCourseGradesView(courseId: number, userId: number, name?: string): Promise { + async logCourseGradesView(courseId: number, userId: number): Promise { userId = userId || CoreSites.getCurrentSiteUserId(); - const wsName = 'gradereport_user_view_grade_report'; - - if (!name) { - // eslint-disable-next-line promise/catch-or-return - CoreCourses.getUserCourse(courseId, true) - .catch(() => ({})) - .then(course => CorePushNotifications.logViewEvent( - courseId, - 'fullname' in course ? course.fullname : '', - 'grades', - wsName, - { userid: userId }, - )); - } else { - CorePushNotifications.logViewEvent(courseId, name, 'grades', wsName, { userid: userId }); - } - const site = CoreSites.getCurrentSite(); const params: CoreGradesGradereportViewGradeReportWSParams = { courseid: courseId, userid: userId }; - await site?.write(wsName, params); + await site?.write('gradereport_user_view_grade_report', params); } /** @@ -403,8 +384,6 @@ export class CoreGradesProvider { courseid: courseId, }; - CorePushNotifications.logViewListEvent('grades', 'gradereport_overview_view_grade_report', params); - const site = CoreSites.getCurrentSite(); await site?.write('gradereport_overview_view_grade_report', params); diff --git a/src/core/features/h5p/components/h5p-iframe/h5p-iframe.ts b/src/core/features/h5p/components/h5p-iframe/h5p-iframe.ts index ea7bb3e38..080140489 100644 --- a/src/core/features/h5p/components/h5p-iframe/h5p-iframe.ts +++ b/src/core/features/h5p/components/h5p-iframe/h5p-iframe.ts @@ -29,6 +29,7 @@ import { CoreSite } from '@classes/site'; import { CoreLogger } from '@singletons/logger'; import { CoreH5PCore, CoreH5PDisplayOptions } from '../../classes/core'; import { CoreH5PHelper } from '../../classes/helper'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Component to render an iframe with an H5P package. @@ -64,7 +65,6 @@ export class CoreH5PIframeComponent implements OnChanges, OnDestroy { public elementRef: ElementRef, router: Router, ) { - this.logger = CoreLogger.getInstance('CoreH5PIframeComponent'); this.site = CoreSites.getRequiredCurrentSite(); this.siteId = this.site.getId(); @@ -101,6 +101,12 @@ export class CoreH5PIframeComponent implements OnChanges, OnDestroy { protected async play(): Promise { let localUrl: string | undefined; let state: string; + this.onlinePlayerUrl = this.onlinePlayerUrl || CoreH5P.h5pPlayer.calculateOnlinePlayerUrl( + this.site.getURL(), + this.fileUrl || '', + this.displayOptions, + this.trackComponent, + ); if (this.fileUrl) { state = await CoreFilepool.getFileStateByUrl(this.siteId, this.fileUrl); @@ -117,14 +123,16 @@ export class CoreH5PIframeComponent implements OnChanges, OnDestroy { if (localUrl) { // Local package. this.iframeSrc = localUrl; - } else { - this.onlinePlayerUrl = this.onlinePlayerUrl || CoreH5P.h5pPlayer.calculateOnlinePlayerUrl( - this.site.getURL(), - this.fileUrl || '', - this.displayOptions, - this.trackComponent, - ); + // Only log analytics event when playing local package, online package already logs it. + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'core_h5p_get_trusted_h5p_file', + name: 'H5P content', + data: { category: 'h5p' }, + url: this.onlinePlayerUrl, + }); + } else { // Never allow downloading in the app. This will only work if the user is allowed to change the params. const src = this.onlinePlayerUrl.replace( CoreH5PCore.DISPLAY_OPTION_DOWNLOAD + '=1', diff --git a/src/core/features/login/pages/site-policy/site-policy.ts b/src/core/features/login/pages/site-policy/site-policy.ts index 0d7f20d70..4131a118b 100644 --- a/src/core/features/login/pages/site-policy/site-policy.ts +++ b/src/core/features/login/pages/site-policy/site-policy.ts @@ -22,6 +22,8 @@ import { CoreLoginHelper } from '@features/login/services/login-helper'; import { CoreSite } from '@classes/site'; import { CoreNavigator } from '@services/navigator'; import { CoreEvents } from '@singletons/events'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { Translate } from '@singletons'; /** * Page to accept a site policy. @@ -94,6 +96,14 @@ export class CoreLoginSitePolicyPage implements OnInit { } finally { this.policyLoaded = true; } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'auth_email_get_signup_settings', + name: Translate.instant('core.login.policyagreement'), + data: { category: 'policy' }, + url: '/user/policy.php', + }); } /** diff --git a/src/core/features/pushnotifications/services/pushnotifications.ts b/src/core/features/pushnotifications/services/pushnotifications.ts index b5b656d6c..16edd45f3 100644 --- a/src/core/features/pushnotifications/services/pushnotifications.ts +++ b/src/core/features/pushnotifications/services/pushnotifications.ts @@ -380,7 +380,6 @@ export class CorePushNotificationsProvider { itemCategory: string | undefined, wsName: string, data?: Record, - siteId?: string, ): Promise { data = data || {}; data.id = itemId; @@ -405,7 +404,6 @@ export class CorePushNotificationsProvider { itemCategory: string, wsName: string, data?: Record, - siteId?: string, ): Promise { data = data || {}; data.moodleaction = wsName; diff --git a/src/core/features/reportbuilder/components/report-detail/report-detail.ts b/src/core/features/reportbuilder/components/report-detail/report-detail.ts index 873dc76f9..9a445b942 100644 --- a/src/core/features/reportbuilder/components/report-detail/report-detail.ts +++ b/src/core/features/reportbuilder/components/report-detail/report-detail.ts @@ -21,6 +21,7 @@ import { REPORT_ROWS_LIMIT, } from '@features/reportbuilder/services/reportbuilder'; import { IonRefresher } from '@ionic/angular'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { CoreNavigator } from '@services/navigator'; import { CoreScreen } from '@services/screen'; import { CoreSites } from '@services/sites'; @@ -28,6 +29,7 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextErrorObject } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; +import { CoreTime } from '@singletons/time'; import { BehaviorSubject, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -63,6 +65,8 @@ export class CoreReportBuilderReportDetailComponent implements OnInit { isString = (value: unknown): boolean => CoreReportBuilder.isString(value); + protected logView: (report: CoreReportBuilderRetrieveReportMapped) => void; + constructor() { this.source$ = this.state$.pipe( map(state => { @@ -72,6 +76,18 @@ export class CoreReportBuilderReportDetailComponent implements OnInit { return source ?? 'system'; }), ); + + this.logView = CoreTime.once(async (report) => { + await CoreUtils.ignoreErrors(CoreReportBuilder.viewReport(this.reportId)); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'core_reportbuilder_view_report', + name: report.details.name, + data: { id: this.reportId, category: 'reportbuilder' }, + url: `/reportbuilder/view.php?id=${this.reportId}`, + }); + }); } /** @@ -105,14 +121,13 @@ export class CoreReportBuilderReportDetailComponent implements OnInit { return; } - await CoreReportBuilder.viewReport(this.reportId); - this.updateState({ report, cardVisibleColumns: report.details.settingsdata.cardviewVisibleColumns, cardviewShowFirstTitle: report.details.settingsdata.cardviewShowFirstTitle, }); + this.logView(report); this.onReportLoaded.emit(report.details); } catch { const errorConfig: CoreTextErrorObject = { diff --git a/src/core/features/reportbuilder/pages/list/list.ts b/src/core/features/reportbuilder/pages/list/list.ts index 2538cb260..b57ce261f 100644 --- a/src/core/features/reportbuilder/pages/list/list.ts +++ b/src/core/features/reportbuilder/pages/list/list.ts @@ -18,9 +18,12 @@ import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/ import { CoreReportBuilderReportsSource } from '@features/reportbuilder/classes/reports-source'; import { CoreReportBuilder, CoreReportBuilderReport, REPORTS_LIST_LIMIT } from '@features/reportbuilder/services/reportbuilder'; import { IonRefresher } from '@ionic/angular'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { CoreNavigator } from '@services/navigator'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; +import { Translate } from '@singletons'; +import { CoreTime } from '@singletons/time'; import { BehaviorSubject } from 'rxjs'; @Component({ @@ -39,7 +42,11 @@ export class CoreReportBuilderListPage implements AfterViewInit, OnDestroy { loadMoreError: false, }); + protected logView: () => void; + constructor() { + this.logView = CoreTime.once(() => this.performLogView()); + try { const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(CoreReportBuilderReportsSource, []); this.reports = new CoreListItemsManager(source, CoreReportBuilderListPage); @@ -71,6 +78,8 @@ export class CoreReportBuilderListPage implements AfterViewInit, OnDestroy { async fetchReports(reload: boolean): Promise { reload ? await this.reports.reload() : await this.reports.load(); this.updateState({ loadMoreError: false }); + + this.logView(); } /** @@ -111,6 +120,19 @@ export class CoreReportBuilderListPage implements AfterViewInit, OnDestroy { await ionRefresher?.complete(); } + /** + * Log view. + */ + protected performLogView(): void { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_reportbuilder_list_reports', + name: Translate.instant('core.reportbuilder.reports'), + data: { category: 'reportbuilder' }, + url: '/reportbuilder/index.php', + }); + } + /** * @inheritdoc */ diff --git a/src/core/features/sitehome/pages/index/index.ts b/src/core/features/sitehome/pages/index/index.ts index ec1432416..a7f41db8a 100644 --- a/src/core/features/sitehome/pages/index/index.ts +++ b/src/core/features/sitehome/pages/index/index.ts @@ -29,6 +29,8 @@ import { CoreCourseModulePrefetchDelegate } from '@features/course/services/modu import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; import { CoreBlockHelper } from '@features/block/services/block-helper'; import { CoreUtils } from '@services/utils/utils'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays site home index. @@ -54,13 +56,25 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy { newsForumModule?: CoreCourseModuleData; protected updateSiteObserver: CoreEventObserver; - protected fetchSuccess = false; + protected logView: () => void; constructor() { // Refresh the enabled flags if site is updated. this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => { this.searchEnabled = !CoreCourses.isSearchCoursesDisabledInSite(); }, CoreSites.getCurrentSiteId()); + + this.logView = CoreTime.once(async () => { + await CoreUtils.ignoreErrors(CoreCourse.logView(this.siteHomeId)); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'core_course_view_course', + name: this.currentSite.getInfo()?.sitename ?? '', + data: { id: this.siteHomeId, category: 'course' }, + url: '/?redirect=0', + }); + }); } /** @@ -138,15 +152,7 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy { this.hasContent = result.hasContent || this.hasContent; } - if (!this.fetchSuccess) { - this.fetchSuccess = true; - CoreUtils.ignoreErrors(CoreCourse.logView( - this.siteHomeId, - undefined, - undefined, - this.currentSite.getInfo()?.sitename, - )); - } + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'core.course.couldnotloadsectioncontent', true); } diff --git a/src/core/features/tag/pages/index/index.ts b/src/core/features/tag/pages/index/index.ts index 09a40f97f..84dcd5bb9 100644 --- a/src/core/features/tag/pages/index/index.ts +++ b/src/core/features/tag/pages/index/index.ts @@ -19,6 +19,10 @@ import { CoreTag } from '@features/tag/services/tag'; import { CoreTagAreaDelegate } from '@features/tag/services/tag-area-delegate'; import { CoreScreen } from '@services/screen'; import { CoreNavigator } from '@services/navigator'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { Translate } from '@singletons'; +import { CoreUrlUtils } from '@services/utils/url'; /** * Page that displays the tag index. @@ -42,6 +46,28 @@ export class CoreTagIndexPage implements OnInit { areas: CoreTagAreaDisplay[] = []; + protected logView: () => void; + + constructor() { + this.logView = CoreTime.once(async () => { + const params = { + tc: this.collectionId || undefined, + tag: this.tagName || undefined, + ta: this.areaId || undefined, + from: this.fromContextId || undefined, + ctx: this.contextId || undefined, + }; + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_tag_get_tagindex_per_area', + name: this.tagName || Translate.instant('core.tag.tag'), + data: { id: this.tagId || undefined, ...params, category: 'tag' }, + url: CoreUrlUtils.addParamsToUrl('/tag/index.php', params), + }); + }); + } + /** * @inheritdoc */ @@ -111,6 +137,8 @@ export class CoreTagIndexPage implements OnInit { this.areas = areasDisplay; + this.logView(); + } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error loading tag index'); } diff --git a/src/core/features/tag/pages/search/search.ts b/src/core/features/tag/pages/search/search.ts index 02ad43eb3..5696f12d5 100644 --- a/src/core/features/tag/pages/search/search.ts +++ b/src/core/features/tag/pages/search/search.ts @@ -24,6 +24,8 @@ import { Translate } from '@singletons'; import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; import { CoreNavigator } from '@services/navigator'; import { CoreMainMenuDeepLinkManager } from '@features/mainmenu/classes/deep-link-manager'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays most used tags and allows searching. @@ -42,6 +44,20 @@ export class CoreTagSearchPage implements OnInit { loaded = false; searching = false; + protected logView: () => void; + + constructor() { + this.logView = CoreTime.once(async () => { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_tag_get_tag_cloud', + name: Translate.instant('core.tag.searchtags'), + data: { category: 'tag' }, + url: '/tag/search.php', + }); + }); + } + /** * View loaded. */ @@ -63,6 +79,10 @@ export class CoreTagSearchPage implements OnInit { this.fetchCollections(), this.fetchTags(), ]); + + if (!this.query) { + this.logView(); + } } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error loading tags.'); } diff --git a/src/core/features/user/pages/participants/participants.page.ts b/src/core/features/user/pages/participants/participants.page.ts index d851cefd9..78c4d81ca 100644 --- a/src/core/features/user/pages/participants/participants.page.ts +++ b/src/core/features/user/pages/participants/participants.page.ts @@ -24,6 +24,8 @@ import { CoreUser, CoreUserParticipant, CoreUserData } from '@features/user/serv import { CoreUtils } from '@services/utils/utils'; import { CoreUserParticipantsSource } from '@features/user/classes/participants-source'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { Translate } from '@singletons'; /** * Page that displays the list of course participants. @@ -195,7 +197,15 @@ class CoreUserParticipantsManager extends CoreListItemsManager { - await CoreUser.logParticipantsView(this.getSource().COURSE_ID); + await CoreUtils.ignoreErrors(CoreUser.logParticipantsView(this.getSource().COURSE_ID)); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_user_view_user_list', + name: Translate.instant('core.user.participants'), + data: { courseid: this.getSource().COURSE_ID, category: 'user' }, + url: `/user/index.php?id=${this.getSource().COURSE_ID}`, + }); } } diff --git a/src/core/features/user/pages/profile/profile.ts b/src/core/features/user/pages/profile/profile.ts index 3cb53f79a..cf8197521 100644 --- a/src/core/features/user/pages/profile/profile.ts +++ b/src/core/features/user/pages/profile/profile.ts @@ -35,6 +35,9 @@ import { CoreCourses } from '@features/courses/services/courses'; import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; import { CoreUserParticipantsSource } from '@features/user/classes/participants-source'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { Translate } from '@singletons'; @Component({ selector: 'page-core-user-profile', @@ -48,7 +51,7 @@ export class CoreUserProfilePage implements OnInit, OnDestroy { protected site!: CoreSite; protected obsProfileRefreshed: CoreEventObserver; protected subscription?: Subscription; - protected fetchSuccess = false; + protected logView: (user: CoreUserProfile) => void; userLoaded = false; isLoadingHandlers = false; @@ -72,6 +75,29 @@ export class CoreUserProfilePage implements OnInit, OnDestroy { this.user.email = data.user.email; this.user.address = CoreUserHelper.formatAddress('', data.user.city, data.user.country); }, CoreSites.getCurrentSiteId()); + + this.logView = CoreTime.once(async (user) => { + try { + await CoreUser.logView(this.userId, this.courseId, user.fullname); + } catch (error) { + this.isDeleted = error?.errorcode === 'userdeleted' || error?.errorcode === 'wsaccessuserdeleted'; + this.isSuspended = error?.errorcode === 'wsaccessusersuspended'; + this.isEnrolled = error?.errorcode !== 'notenrolledprofile'; + } + let extraParams = ''; + if (this.userId !== CoreSites.getCurrentSiteUserId()) { + const isCourseProfile = this.courseId && this.courseId !== CoreSites.getCurrentSiteHomeId(); + extraParams = `?id=${this.userId}` + (isCourseProfile ? `&course=${this.courseId}` : ''); + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'core_user_view_user_profile', + name: user.fullname + ': ' + Translate.instant('core.publicprofile'), + data: { id: this.userId, courseid: this.courseId || undefined, category: 'user' }, + url: `/user/profile.php${extraParams}`, + }); + }); } /** @@ -151,18 +177,7 @@ export class CoreUserProfilePage implements OnInit, OnDestroy { this.isLoadingHandlers = !CoreUserDelegate.areHandlersLoaded(user.id, context, this.courseId); }); - if (!this.fetchSuccess) { - this.fetchSuccess = true; - - try { - await CoreUser.logView(this.userId, this.courseId, this.user.fullname); - } catch (error) { - this.isDeleted = error?.errorcode === 'userdeleted' || error?.errorcode === 'wsaccessuserdeleted'; - this.isSuspended = error?.errorcode === 'wsaccessusersuspended'; - this.isEnrolled = error?.errorcode !== 'notenrolledprofile'; - } - } - + this.logView(user); } catch (error) { // Error is null for deleted users, do not show the modal. CoreDomUtils.showErrorModal(error); diff --git a/src/core/features/user/services/user.ts b/src/core/features/user/services/user.ts index 4e80ccf32..65ca5a1aa 100644 --- a/src/core/features/user/services/user.ts +++ b/src/core/features/user/services/user.ts @@ -26,7 +26,6 @@ import { CoreEvents, CoreEventSiteData, CoreEventUserDeletedData, CoreEventUserS import { CoreStatusWithWarningsWSResponse, CoreWSExternalWarning } from '@services/ws'; import { CoreError } from '@classes/errors/error'; import { USERS_TABLE_NAME, CoreUserDBRecord } from './database/user'; -import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; import { CoreUserHelper } from './user-helper'; import { CoreUrl } from '@singletons/url'; @@ -569,24 +568,20 @@ export class CoreUserProvider { * * @param userId User ID. * @param courseId Course ID. - * @param name Name of the user. * @returns Promise resolved when done. */ - async logView(userId: number, courseId?: number, name?: string, siteId?: string): Promise { + async logView(userId: number, courseId?: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); const params: CoreUserViewUserProfileWSParams = { userid: userId, }; - const wsName = 'core_user_view_user_profile'; if (courseId) { params.courseid = courseId; } - CorePushNotifications.logViewEvent(userId, name, 'user', wsName, { courseid: courseId }); - - return site.write(wsName, params); + return site.write('core_user_view_user_profile', params); } /** @@ -602,8 +597,6 @@ export class CoreUserProvider { courseid: courseId, }; - CorePushNotifications.logViewListEvent('user', 'core_user_view_user_list', params); - return site.write('core_user_view_user_list', params); } diff --git a/src/core/lang.json b/src/core/lang.json index fc5ff75b5..dfbbe7b2c 100644 --- a/src/core/lang.json +++ b/src/core/lang.json @@ -248,6 +248,7 @@ "play": "Play", "previous": "Previous", "proceed": "Proceed", + "publicprofile": "Public profile", "pulltorefresh": "Pull to refresh", "qrscanner": "QR scanner", "quotausage": "You have currently used {{$a.used}} of your {{$a.total}} limit.", diff --git a/upgrade.txt b/upgrade.txt index 06cc09197..dcb5816f9 100644 --- a/upgrade.txt +++ b/upgrade.txt @@ -6,6 +6,7 @@ information provided here is intended especially for developers. - CoreSiteBasicInfo fullName attribute has changed to fullname and avatar to userpictureurl to match user fields. - Font Awesome icon library has been updated to 6.4.0. But nothing has changed, only version number. - The analytics system in the app has been refactored and some functions that could trigger analytics calls no longer do it, now you need to use CoreAnalytics instead. Some functions in CoreCourseLogHelper and CorePushNotificationsProvider have been deprecated. + - Due to the analytics refactor, the parameters of most log functions have changed. === 4.2.0 ===