diff --git a/scripts/langindex.json b/scripts/langindex.json index a0e8aad4bf7..523dd673afb 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", @@ -2347,9 +2352,9 @@ "core.settings.disabled": "lesson", "core.settings.disallowed": "message", "core.settings.displayformat": "local_moodlemobileapp", + "core.settings.enableanalytics": "local_moodlemobileapp", + "core.settings.enableanalyticsdescription": "local_moodlemobileapp", "core.settings.enabledownloadsection": "local_moodlemobileapp", - "core.settings.enablefirebaseanalytics": "local_moodlemobileapp", - "core.settings.enablefirebaseanalyticsdescription": "local_moodlemobileapp", "core.settings.enablerichtexteditor": "local_moodlemobileapp", "core.settings.enablerichtexteditordescription": "local_moodlemobileapp", "core.settings.encryptedpushsupported": "local_moodlemobileapp", diff --git a/src/addons/badges/pages/issued-badge/issued-badge.ts b/src/addons/badges/pages/issued-badge/issued-badge.ts index 890ccbfe091..1440e7f02db 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 31ba89d84f3..9d02a2c5f2f 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 11d2b32c6a0..b5ee6033597 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 83e588deb18..c3491951e98 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 0295e9d1523..1d9c2eea985 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 4586fd25958..909169ad8ef 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 c448ad2dbe4..8820fd71535 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 4ad52ea63f9..1f539005c66 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 16eb9076aec..e2c01de02b5 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 d3a9f825b99..bf14914aca7 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 28554cdb74b..1495a9e6e27 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 d091bd47015..cf780f27d77 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 85d06ca3032..5c57a659bb5 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 eb314632a00..991aa9690bd 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 4991f034f6a..7f83cc0c2eb 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 b95bb88b5e8..b58921aa386 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 e63a1f1a5b6..638ba22153e 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 de0b0cc9dfd..116924c0b89 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 634d79773bb..b7677e45171 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 1670f1dec3b..d0a59e2e03f 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 847d3502dd2..6749d9f1b6a 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 c213cb8eba5..ce34faf773d 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 72ad86990e1..92914b8ae26 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 a592188532b..dd1e40225a5 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 93c64972f41..aa77f2d33af 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 2bd8a6cbea4..3d22145cff0 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 2365c52919b..19a86c64361 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 7315bdf703f..b298143c046 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 8c13924cfa3..e1c168c10eb 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 c59e8af912b..0e98536de94 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 9f78793fda6..32f91b9fa5f 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 4b508958fb4..2c631423a68 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 d4f7e039cd9..e119299fa3f 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 57e002fea28..055b7affb03 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 64e0ca22ecd..3b6c939d66c 100644 --- a/src/addons/mod/data/components/index/index.ts +++ b/src/addons/mod/data/components/index/index.ts @@ -45,6 +45,8 @@ import { AddonModDataModuleHandlerService } from '../../services/handlers/module import { AddonModDataPrefetchHandler } from '../../services/handlers/prefetch'; import { AddonModDataComponentsCompileModule } from '../components-compile.module'; import { AddonModDataSearchComponent } from '../search/search'; +import { CoreUrlUtils } from '@services/utils/url'; +import { CoreTime } from '@singletons/time'; const contentToken = ''; @@ -59,7 +61,7 @@ const contentToken = ''; export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit, OnDestroy { component = AddonModDataProvider.COMPONENT; - moduleName = 'data'; + pluginName = 'data'; access?: AddonModDataGetDataAccessInformationWSResponse; database?: AddonModDataData; @@ -114,6 +116,7 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp protected entryChangedObserver?: CoreEventObserver; protected ratingOfflineObserver?: CoreEventObserver; protected ratingSyncObserver?: CoreEventObserver; + protected logSearch?: () => void; constructor( protected content?: IonContent, @@ -404,6 +407,7 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp // Add data to search object. if (modalData) { this.search = modalData; + this.logSearch = CoreTime.once(() => this.performLogSearch()); this.searchEntries(0); } } @@ -420,8 +424,8 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp try { await this.fetchEntriesData(); - // Log activity view for coherence with Moodle web. - await this.logActivity(); + + this.logSearch?.(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true); } finally { @@ -470,9 +474,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 +536,34 @@ 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'); + } + + /** + * Log search. + */ + protected async performLogSearch(): Promise { + if (!this.database || !this.search.searching) { + return; + } + + const params: Record = { + perpage: AddonModDataProvider.PER_PAGE, + search: !this.search.searchingAdvanced ? this.search.text : '', + sort: this.search.sortBy, + order: this.search.sortDirection, + advanced: this.search.searchingAdvanced ? 1 : 0, + filter: 1, + }; + + // @todo: Add advanced search parameters. Leave them empty if not using advanced search. + + this.analyticsLogEvent('mod_data_search_entries', { + data: params, + url: CoreUrlUtils.addParamsToUrl(`/mod/data/view.php?d=${this.database.id}`, params), + }); } /** diff --git a/src/addons/mod/data/pages/edit/edit.ts b/src/addons/mod/data/pages/edit/edit.ts index e6452eab58d..4946a68006a 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 681ffbda0ce..0fff2c13133 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 0ff632a1751..a433b1bc0d5 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 fe3c6f74d83..e08133127a8 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 e9e4defceac..7980f364040 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 e2db4042380..b80e42b796f 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 3799bb66e42..817fd25007e 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 ad96737b0d8..0c45198a10a 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 6917682bad3..2046106ff0a 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 c38e62ff8ed..8dd3fcf9d6d 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 709d928150e..40daf46937e 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 701e0f56dd5..ab4f8f4507c 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 bb604e3f212..3283edc3336 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 bb06dcce1c7..60117a03f8e 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 cdb7772e945..6978faa24b0 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; - - constructor(protected route: ActivatedRoute, @Optional() protected splitView: CoreSplitViewComponent) {} + protected logView: () => void; + + 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 d20f88efca8..650dbefcc30 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 3ca1498ca92..56f5c4b87c6 100644 --- a/src/addons/mod/glossary/components/index/index.ts +++ b/src/addons/mod/glossary/components/index/index.ts @@ -56,6 +56,7 @@ import { import { AddonModGlossaryModuleHandlerService } from '../../services/handlers/module'; import { AddonModGlossaryPrefetchHandler } from '../../services/handlers/prefetch'; import { AddonModGlossaryModePickerPopoverComponent } from '../mode-picker/mode-picker'; +import { CoreTime } from '@singletons/time'; /** * Component that displays a glossary entry page. @@ -71,7 +72,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; component = AddonModGlossaryProvider.COMPONENT; - moduleName = 'glossary'; + pluginName = 'glossary'; canAdd = false; loadMoreError = false; @@ -86,6 +87,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity protected sourceUnsubscribe?: () => void; protected observers?: CoreEventObserver[]; protected checkCompletionAfterLog = false; // Use CoreListItemsManager log system instead. + protected logSearch?: () => void; getDivider?: (entry: AddonModGlossaryEntry) => string; showDivider: (entry: AddonModGlossaryEntry, previous?: AddonModGlossaryEntry) => boolean = () => false; @@ -226,6 +228,10 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity this.hasOfflineRatings = hasOfflineRatings; this.hasOffline = this.hasOfflineEntries || this.hasOfflineRatings; + + if (this.isSearch && this.logSearch) { + this.logSearch(); + } } /** @@ -424,11 +430,23 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity search(query: string): void { this.loadingMessage = Translate.instant('core.searching'); this.showLoading = true; + this.logSearch = CoreTime.once(() => this.performLogSearch(query)); this.entries?.getSource().search(query); this.loadContent(); } + /** + * Log search. + * + * @param query Text entered on the search box. + */ + protected async performLogSearch(query: string): Promise { + this.analyticsLogEvent('mod_glossary_get_entries_by_search', { + data: { mode: 'search', hook: query, fullsearch: 1 }, + }); + } + /** * @inheritdoc */ @@ -482,12 +500,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 48754463971..fef56bab530 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 b84fc38bd47..7af2521021a 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 e54f4017e6a..3142c96372d 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 e91c957bdbc..f0c2c9faff8 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 cf977abbf41..f5eff6eb1e6 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 594e0d20105..5a23aa738a6 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 bde54ea6920..29e111c8a89 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 4c388cf7826..f71cf119380 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 7d89c20048d..b4b6bcc98b8 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 2edc3daf7fd..99ca572a28a 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 792c6477dd1..7ea3706a7a0 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 27e780f1346..af0b4945164 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 333f0bb4813..c1870a741be 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 2d5aaa00b87..9b0ab35e7b6 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 357510506bf..989432eab32 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 957a67fe920..d139e047326 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 6a266342c71..19579a5e8a1 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 b9390ec91de..cf5d867e5d5 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 b70daf5f816..0ea6316c0a5 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 a5c789a0c8e..63eeaf7d7d3 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 74871a22e3e..1dd59d96cce 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 95fefe96ca0..3582a144ac3 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 3871a2798f4..0830bae6a21 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 11e75b04189..e781f0b545d 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 10c6e1063a5..2b590b2c60c 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 90e36e1ed6a..8fca36f6c5a 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 5e6e79137ef..aaa42569abb 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 424aacfab31..93e79b53699 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 8fa968ddabe..287830ed891 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 7ae3d4f528a..4a720235695 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 de2899f36f4..0d2cea0c12e 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 60c044bf8c0..c4ea9f52a93 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 404138cbaa1..216ac680c2a 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 f3d82dc3e0d..16dc33583cc 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 87cf410a0a2..9884350b0d9 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 6c2c1b96a8f..86bd71e2631 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 62932720caf..dc79309ebcc 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 cfed3a329f0..8b7c54776f9 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 961591bac80..973a16a0317 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 d83fd9d4c55..4c7ea2d08d3 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 711c20fb529..faf95933b7a 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 26c14809d79..a70f23a5598 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 1369ec3080b..58bf0e68e5f 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 71e4d3c067d..0c2cfd8db4b 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 7a5b4edb846..a8f6258df27 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 b41f96611d5..f0c7a5d5603 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 6cf24b9f772..4eca6fd5ff3 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/delegate.ts b/src/core/classes/delegate.ts index 799f0cc17d9..adc2023f036 100644 --- a/src/core/classes/delegate.ts +++ b/src/core/classes/delegate.ts @@ -207,6 +207,15 @@ export class CoreDelegate { return enabled ? this.enabledHandlers[name] !== undefined : this.handlers[name] !== undefined; } + /** + * Check if the delegate has at least 1 registered handler (not necessarily enabled). + * + * @returns If there is at least 1 handler. + */ + hasHandlers(): boolean { + return Object.keys(this.handlers).length > 0; + } + /** * Check if a time belongs to the last update handlers call. * This is to handle the cases where updateHandlers don't finish in the same order as they're called. diff --git a/src/core/classes/items-management/list-items-manager.ts b/src/core/classes/items-management/list-items-manager.ts index 83c46688265..3ec31d484ea 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/classes/site.ts b/src/core/classes/site.ts index 0fe84afb1b6..c5339634bbc 100644 --- a/src/core/classes/site.ts +++ b/src/core/classes/site.ts @@ -33,7 +33,7 @@ import { import { CoreDomUtils, ToastDuration } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; -import { CoreUrlUtils, CoreUrlParams } from '@services/utils/url'; +import { CoreUrlUtils } from '@services/utils/url'; import { CoreUtils, CoreUtilsOpenInBrowserOptions } from '@services/utils/utils'; import { CoreConstants } from '@/core/constants'; import { SQLiteDB } from '@classes/sqlitedb'; @@ -63,6 +63,7 @@ import { firstValueFrom } from '../utils/rxjs'; import { CoreSiteError } from '@classes/errors/siteerror'; import { CoreUserAuthenticatedSupportConfig } from '@features/user/classes/support/authenticated-support-config'; import { CoreLoginHelper } from '@features/login/services/login-helper'; +import { CorePath } from '@singletons/path'; /** * QR Code type enumeration. @@ -1598,8 +1599,8 @@ export class CoreSite { * @param anchor Anchor text if needed. * @returns URL with params. */ - createSiteUrl(path: string, params?: CoreUrlParams, anchor?: string): string { - return CoreUrlUtils.addParamsToUrl(this.siteUrl + path, params, anchor); + createSiteUrl(path: string, params?: Record, anchor?: string): string { + return CoreUrlUtils.addParamsToUrl(CorePath.concatenatePaths(this.siteUrl, path), params, anchor); } /** @@ -1891,12 +1892,12 @@ export class CoreSite { options.showBrowserWarning = false; // A warning already shown, no need to show another. } + options.originalUrl = url; + // Open the URL. if (inApp) { return CoreUtils.openInApp(autoLoginUrl, options); } else { - options.browserWarningUrl = url; - return CoreUtils.openInBrowser(autoLoginUrl, options); } } diff --git a/src/core/features/course/classes/main-activity-component.ts b/src/core/features/course/classes/main-activity-component.ts index e2a8d20fd4b..02f2e812733 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 5944a2a3b86..0e3761b1e23 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 697cdd3312e..381c6040a84 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 b510fc6b3f7..8185b598982 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 e0eb86ae3be..c27314dcc3e 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 f7506171e46..6788eb6994e 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 fe87cd189f3..f9653eeebaf 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/course/services/log-helper.ts b/src/core/features/course/services/log-helper.ts index 83b2ac5be72..052149036f5 100644 --- a/src/core/features/course/services/log-helper.ts +++ b/src/core/features/course/services/log-helper.ts @@ -19,7 +19,6 @@ import { CoreSites } from '@services/sites'; import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUtils } from '@services/utils/utils'; -import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; import { makeSingleton } from '@singletons'; import { ACTIVITY_LOG_TABLE, CoreCourseActivityLogDBRecord } from './database/log'; import { CoreStatusWithWarningsWSResponse } from '@services/ws'; @@ -190,7 +189,6 @@ export class CoreCourseLogHelperProvider { /** * Perform log online. Data will be saved offline for syncing. - * It also triggers a Firebase view_item event. * * @param ws WS name. * @param data Data to send to the WS. @@ -198,9 +196,10 @@ export class CoreCourseLogHelperProvider { * @param componentId Component ID. * @param name Name of the viewed item. * @param category Category of the viewed item. - * @param eventData Data to pass to the Firebase event. + * @param eventData Data to pass to the analytics event. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when done. + * @deprecated since 4.3. Please use CoreCourseLogHelper.log instead. */ logSingle( ws: string, @@ -209,26 +208,24 @@ export class CoreCourseLogHelperProvider { componentId: number, name?: string, category?: string, - eventData?: Record, + eventData?: Record, siteId?: string, ): Promise { - CorePushNotifications.logViewEvent(componentId, name, category, ws, eventData, siteId); - return this.log(ws, data, component, componentId, siteId); } /** * Perform log online. Data will be saved offline for syncing. - * It also triggers a Firebase view_item_list event. * * @param ws WS name. * @param data Data to send to the WS. * @param component Component name. * @param componentId Component ID. * @param category Category of the viewed item. - * @param eventData Data to pass to the Firebase event. + * @param eventData Data to pass to the analytics event. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when done. + * @deprecated since 4.3. Please use CoreCourseLogHelper.log instead. */ logList( ws: string, @@ -236,11 +233,9 @@ export class CoreCourseLogHelperProvider { component: string, componentId: number, category: string, - eventData?: Record, + eventData?: Record, siteId?: string, ): Promise { - CorePushNotifications.logViewListEvent(category, ws, eventData, siteId); - return this.log(ws, data, component, componentId, siteId); } diff --git a/src/core/features/courses/pages/categories/categories.ts b/src/core/features/courses/pages/categories/categories.ts index 903c19012c4..f081616d8fa 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 8bca938e028..cc0ad8382ee 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 3ba71b1263b..f61ba3d0a47 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,8 @@ export class CoreCoursesListPage implements OnInit, OnDestroy { protected downloadEnabledObserver: CoreEventObserver; protected courseIds = ''; protected isDestroyed = false; + protected logView: () => void; + protected logSearch?: () => void; constructor() { this.currentSiteId = CoreSites.getRequiredCurrentSite().getId(); @@ -96,6 +101,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', + }); + } + }); } /** @@ -135,7 +160,7 @@ export class CoreCoursesListPage implements OnInit, OnDestroy { try { if (this.searchMode) { if (this.searchText) { - await this.search(this.searchText); + await this.searchCourses(); } } else { await this.loadCourses(true); @@ -176,6 +201,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); @@ -221,6 +248,7 @@ export class CoreCoursesListPage implements OnInit, OnDestroy { this.courses = []; this.searchPage = 0; this.searchTotal = 0; + this.logSearch = CoreTime.once(() => this.performLogSearch()); const modal = await CoreDomUtils.showModalLoading('core.searching', true); await this.searchCourses().finally(() => { @@ -242,6 +270,23 @@ export class CoreCoursesListPage implements OnInit, OnDestroy { this.fetchCourses(); } + /** + * Log search. + */ + protected async performLogSearch(): Promise { + if (!this.searchMode) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_course_search_courses', + name: Translate.instant('core.courses.availablecourses'), + data: { search: this.searchText, category: 'course' }, + url: `/course/search.php?search=${this.searchText}`, + }); + } + /** * Load more courses. * @@ -279,6 +324,8 @@ export class CoreCoursesListPage implements OnInit, OnDestroy { this.searchPage++; this.canLoadMore = this.courses.length < this.searchTotal; + + this.logSearch?.(); } catch (error) { this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading. !this.isDestroyed && CoreDomUtils.showErrorModalDefault(error, 'core.courses.errorsearching', true); diff --git a/src/core/features/courses/pages/my/my.ts b/src/core/features/courses/pages/my/my.ts index c9d95650008..3f9ab790750 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 7bca24a8772..d148992d1b5 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 dd47d084c9c..79e55d35a55 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 be80ee57493..85ae19c4e24 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 ea7bb3e3870..080140489c3 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 0d7f20d70e0..4131a118b13 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 234e54825c0..16edd45f3fe 100644 --- a/src/core/features/pushnotifications/services/pushnotifications.ts +++ b/src/core/features/pushnotifications/services/pushnotifications.ts @@ -47,6 +47,7 @@ import { CoreDatabaseCachingStrategy, CoreDatabaseTableProxy } from '@classes/da import { CoreObject } from '@singletons/object'; import { lazyMap, LazyMap } from '@/core/utils/lazy-map'; import { CorePlatform } from '@services/platform'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Service to handle push notifications. @@ -133,8 +134,11 @@ export class CorePushNotificationsProvider { CoreLocalNotifications.registerClick( CorePushNotificationsProvider.COMPONENT, (notification) => { - // Log notification open event. - this.logEvent('moodle_notification_open', notification, true); + CoreAnalytics.logEvent({ + eventName: 'moodle_notification_open', + type: CoreAnalyticsEventType.PUSH_NOTIFICATION, + data: notification, + }); this.notificationClicked(notification); }, @@ -145,8 +149,11 @@ export class CorePushNotificationsProvider { 'clear', CorePushNotificationsProvider.COMPONENT, (notification) => { - // Log notification dismissed event. - this.logEvent('moodle_notification_dismiss', notification, true); + CoreAnalytics.logEvent({ + eventName: 'moodle_notification_dismiss', + type: CoreAnalyticsEventType.PUSH_NOTIFICATION, + data: notification, + }); }, ); } @@ -248,26 +255,14 @@ export class CorePushNotificationsProvider { } /** - * Enable or disable Firebase analytics. + * Enable or disable analytics. * * @param enable Whether to enable or disable. * @returns Promise resolved when done. + * @deprecated since 4.3. Use CoreAnalytics.enableAnalytics instead. */ async enableAnalytics(enable: boolean): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const win = window; // This feature is only present in our fork of the plugin. - - if (!CoreConstants.CONFIG.enableanalytics || !win.PushNotification?.enableAnalytics) { - return; - } - - await new Promise(resolve => { - win.PushNotification.enableAnalytics(resolve, (error) => { - this.logger.error('Error enabling or disabling Firebase analytics', enable, error); - - resolve(); - }, !!enable); - }); + return CoreAnalytics.enableAnalytics(enable); } /** @@ -340,37 +335,35 @@ export class CorePushNotificationsProvider { } /** - * Log a firebase event. + * Log an analytics event. * - * @param name Name of the event. + * @param eventName Name of the event. * @param data Data of the event. - * @param filter Whether to filter the data. This is useful when logging a full notification. * @returns Promise resolved when done. This promise is never rejected. + * @deprecated since 4.3. Use CoreAnalytics.logEvent instead. */ - async logEvent(name: string, data: Record, filter?: boolean): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const win = window; // This feature is only present in our fork of the plugin. - - if (!CoreConstants.CONFIG.enableanalytics || !win.PushNotification?.logEvent) { - return; + async logEvent(eventName: string, data: Record): Promise { + if (eventName !== 'view_item' && eventName !== 'view_item_list') { + return CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.PUSH_NOTIFICATION, + eventName, + data, + }); } - // Check if the analytics is enabled by the user. - const enabled = await CoreConfig.get(CoreConstants.SETTINGS_ANALYTICS_ENABLED, true); - if (!enabled) { - return; - } + const name = data.name ? String(data.name) : ''; + delete data.name; - await new Promise(resolve => { - win.PushNotification.logEvent(resolve, (error) => { - this.logger.error('Error logging firebase event', name, error); - resolve(); - }, name, data, !!filter); + return CoreAnalytics.logEvent({ + type: eventName === 'view_item' ? CoreAnalyticsEventType.VIEW_ITEM : CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: data.moodleaction ?? '', + name, + data, }); } /** - * Log a firebase view_item event. + * Log an analytics VIEW_ITEM_LIST event. * * @param itemId The item ID. * @param itemName The item name. @@ -379,57 +372,44 @@ export class CorePushNotificationsProvider { * @param data Other data to pass to the event. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when done. This promise is never rejected. + * @deprecated since 4.3. Use CoreAnalytics.logEvent instead. */ logViewEvent( itemId: number | string | undefined, itemName: string | undefined, itemCategory: string | undefined, wsName: string, - data?: Record, - siteId?: string, + data?: Record, ): Promise { data = data || {}; - - // Add "moodle" to the name of all extra params. - data = CoreUtils.prefixKeys(data, 'moodle'); + data.id = itemId; + data.name = itemName; + data.category = itemCategory; data.moodleaction = wsName; - data.moodlesiteid = siteId || CoreSites.getCurrentSiteId(); - - if (itemId) { - data.item_id = itemId; - } - if (itemName) { - data.item_name = itemName; - } - if (itemCategory) { - data.item_category = itemCategory; - } - return this.logEvent('view_item', data, false); + return this.logEvent('view_item', data); } /** - * Log a firebase view_item_list event. + * Log an analytics view item list event. * * @param itemCategory The item category. * @param wsName Name of the WS. * @param data Other data to pass to the event. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when done. This promise is never rejected. + * @deprecated since 4.3. Use CoreAnalytics.logEvent instead. */ - logViewListEvent(itemCategory: string, wsName: string, data?: Record, siteId?: string): Promise { + logViewListEvent( + itemCategory: string, + wsName: string, + data?: Record, + ): Promise { data = data || {}; - - // Add "moodle" to the name of all extra params. - data = CoreUtils.prefixKeys(data, 'moodle'); data.moodleaction = wsName; - data.moodlesiteid = siteId || CoreSites.getCurrentSiteId(); - - if (itemCategory) { - data.item_category = itemCategory; - } + data.category = itemCategory; - return this.logEvent('view_item_list', data, false); + return this.logEvent('view_item_list', data); } /** 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 873dc76f9b9..9a445b942a1 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 2538cb26010..b57ce261f49 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/settings/lang.json b/src/core/features/settings/lang.json index 2680792cb75..5af24834ba5 100644 --- a/src/core/features/settings/lang.json +++ b/src/core/features/settings/lang.json @@ -32,9 +32,9 @@ "disabled": "Disabled", "disallowed": "Locked off", "displayformat": "Display format", + "enableanalytics": "Enable analytics", + "enableanalyticsdescription": "If enabled, the app will collect anonymous data usage.", "enabledownloadsection": "Enable download sections", - "enablefirebaseanalytics": "Enable Firebase analytics", - "enablefirebaseanalyticsdescription": "If enabled, the app will collect anonymous data usage.", "enablerichtexteditor": "Enable text editor", "enablerichtexteditordescription": "If enabled, a text editor will be available when entering content.", "encryptedpushsupported": "Encrypted push notifications supported", diff --git a/src/core/features/settings/pages/general/general.html b/src/core/features/settings/pages/general/general.html index 9cc006e5710..6be8e054e69 100644 --- a/src/core/features/settings/pages/general/general.html +++ b/src/core/features/settings/pages/general/general.html @@ -76,8 +76,8 @@

{{ 'core.settings.general' | translate }}

-

{{ 'core.settings.enablefirebaseanalytics' | translate }}

-

{{ 'core.settings.enablefirebaseanalyticsdescription' | translate }}

+

{{ 'core.settings.enableanalytics' | translate }}

+

{{ 'core.settings.enableanalyticsdescription' | translate }}

diff --git a/src/core/features/settings/pages/general/general.ts b/src/core/features/settings/pages/general/general.ts index 874028c8ede..88a498b7770 100644 --- a/src/core/features/settings/pages/general/general.ts +++ b/src/core/features/settings/pages/general/general.ts @@ -27,6 +27,7 @@ import { CoreUtils } from '@services/utils/utils'; import { AlertButton } from '@ionic/angular'; import { CoreNavigator } from '@services/navigator'; import { CorePlatform } from '@services/platform'; +import { CoreAnalytics } from '@services/analytics'; /** * Page that displays the general settings. @@ -101,7 +102,7 @@ export class CoreSettingsGeneralPage { this.debugDisplay = await CoreConfig.get(CoreConstants.SETTINGS_DEBUG_DISPLAY, false); - this.analyticsSupported = CoreConstants.CONFIG.enableanalytics; + this.analyticsSupported = CoreAnalytics.hasHandlers(); if (this.analyticsSupported) { this.analyticsEnabled = await CoreConfig.get(CoreConstants.SETTINGS_ANALYTICS_ENABLED, true); } diff --git a/src/core/features/sitehome/pages/index/index.ts b/src/core/features/sitehome/pages/index/index.ts index ec1432416d2..a7f41db8ab1 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 09a40f97ff4..84dcd5bb9be 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.html b/src/core/features/tag/pages/search/search.html index 6126aa8c3c5..f428692cbd2 100644 --- a/src/core/features/tag/pages/search/search.html +++ b/src/core/features/tag/pages/search/search.html @@ -22,7 +22,7 @@

{{ 'core.tag.searchtags' | translate }}

autocorrect="off" [spellcheck]="false" [autoFocus]="false" [lengthCheck]="0" searchArea="CoreTag"> - + {{ 'core.tag.inalltagcoll' | translate }} diff --git a/src/core/features/tag/pages/search/search.ts b/src/core/features/tag/pages/search/search.ts index 02ad43eb3da..a608bd19732 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,21 @@ export class CoreTagSearchPage implements OnInit { loaded = false; searching = false; + protected logView: () => void; + protected logSearch?: () => 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 +80,10 @@ export class CoreTagSearchPage implements OnInit { this.fetchCollections(), this.fetchTags(), ]); + + if (!this.query) { + this.logView(); + } } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error loading tags.'); } @@ -92,6 +113,8 @@ export class CoreTagSearchPage implements OnInit { */ async fetchTags(): Promise { this.cloud = await CoreTag.getTagCloud(this.collectionId, undefined, undefined, this.query); + + this.logSearch?.(); } /** @@ -120,11 +143,17 @@ export class CoreTagSearchPage implements OnInit { * Search tags. * * @param query Search query. + * @param collectionId Collection ID to use. * @returns Resolved when done. */ - searchTags(query: string): Promise { + searchTags(query: string, collectionId?: number): Promise { this.searching = true; this.query = query; + if (collectionId !== undefined) { + this.collectionId = collectionId; + } + + this.logSearch = CoreTime.once(() => this.performLogSearch()); CoreApp.closeKeyboard(); return this.fetchTags().catch((error) => { @@ -134,4 +163,21 @@ export class CoreTagSearchPage implements OnInit { }); } + /** + * Log search. + */ + protected async performLogSearch(): Promise { + if (!this.query) { + return; + } + + 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&query=${this.query}&tc=${this.collectionId}&go=${Translate.instant('core.search')}`, + }); + } + } diff --git a/src/core/features/user/pages/participants/participants.page.ts b/src/core/features/user/pages/participants/participants.page.ts index d851cefd99c..78c4d81ca30 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 3cb53f79ac9..cf8197521a9 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 4e80ccf322f..65ca5a1aa39 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 fc5ff75b59a..dfbbe7b2cbc 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/src/core/services/analytics.ts b/src/core/services/analytics.ts new file mode 100644 index 00000000000..ae4e5ad29f8 --- /dev/null +++ b/src/core/services/analytics.ts @@ -0,0 +1,184 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; +import { CorePushNotificationsNotificationBasicData } from '@features/pushnotifications/services/pushnotifications'; +import { makeSingleton } from '@singletons'; +import { CoreEvents } from '@singletons/events'; +import { CoreSites } from './sites'; +import { CoreConfig, CoreConfigProvider } from './config'; +import { CoreConstants } from '../constants'; +import { CoreUrlUtils } from './utils/url'; + +/** + * Helper service to support analytics. + */ +@Injectable({ providedIn: 'root' }) +export class CoreAnalyticsService extends CoreDelegate { + + constructor() { + super('CoreAnalyticsService', true); + + CoreEvents.on(CoreConfigProvider.ENVIRONMENT_UPDATED, () => this.updateHandlers()); + CoreEvents.on(CoreEvents.LOGOUT, () => this.clearSiteHandlers()); + } + + /** + * Clear current site handlers. Reserved for core use. + */ + protected clearSiteHandlers(): void { + this.enabledHandlers = {}; + } + + /** + * Enable or disable analytics for all handlers. + * + * @param enable Whether to enable or disable. + * @returns Promise resolved when done. + */ + async enableAnalytics(enable: boolean): Promise { + try { + await Promise.all(Object.values(this.handlers).map(handler => handler.enableAnalytics?.(enable))); + } catch (error) { + this.logger.error(`Error ${enable ? 'enabling' : 'disabling'} analytics`, error); + } + } + + /** + * Log an event for the current site. + * + * @param event Event data. + */ + async logEvent(event: CoreAnalyticsAnyEvent): Promise { + const site = CoreSites.getCurrentSite(); + if (!site) { + return; + } + + // Check if analytics is enabled by the user. + const enabled = await CoreConfig.get(CoreConstants.SETTINGS_ANALYTICS_ENABLED, true); + if (!enabled) { + return; + } + + const treatedEvent: CoreAnalyticsEvent = { + ...event, + siteId: site.getId(), + }; + if ('url' in treatedEvent && treatedEvent.url) { + if (!CoreUrlUtils.isAbsoluteURL(treatedEvent.url)) { + treatedEvent.url = site.createSiteUrl(treatedEvent.url); + } else if (!site.containsUrl(treatedEvent.url)) { + // URL belongs to a different site, ignore the event. + return; + } + } + + try { + await Promise.all(Object.values(this.enabledHandlers).map(handler => handler.logEvent(treatedEvent))); + } catch (error) { + this.logger.error('Error logging event', event, error); + } + } + +} + +export const CoreAnalytics = makeSingleton(CoreAnalyticsService); + +/** + * Interface that all analytics handlers must implement. + */ +export interface CoreAnalyticsHandler extends CoreDelegateHandler { + + /** + * Log an event. + * + * @param event Event data. + */ + logEvent(event: CoreAnalyticsEvent): Promise; + + /** + * Enable or disable analytics. + * + * @param enable Whether to enable or disable. + * @returns Promise resolved when done. + */ + enableAnalytics?(enable: boolean): Promise; + +} + +/** + * Possible types of events. + */ +export enum CoreAnalyticsEventType { + VIEW_ITEM = 'view_item', // View some page or data that mainly contains one item. + VIEW_ITEM_LIST = 'view_item_list', // View some page or data that mainly contains a list of items. + PUSH_NOTIFICATION = 'push_notification', // Event related to push notifications. + DOWNLOAD_FILE = 'download_file', // A file was downloaded. + OPEN_LINK = 'open_link', // A link was opened in browser or InAppBrowser. +} + +/** + * Any type of event data. + */ +export type CoreAnalyticsAnyEvent = CoreAnalyticsViewEvent | CoreAnalyticsPushEvent | CoreAnalyticsDownloadFileEvent | +CoreAnalyticsOpenLinkEvent; + +/** + * Event data, including calculated data. + */ +export type CoreAnalyticsEvent = CoreAnalyticsAnyEvent & { + siteId: string; +}; + +/** + * Data specific for the VIEW_ITEM and VIEW_LIST events. + */ +export type CoreAnalyticsViewEvent = { + type: CoreAnalyticsEventType.VIEW_ITEM | CoreAnalyticsEventType.VIEW_ITEM_LIST; + ws: string; // Name of the WS used to log the data in LMS or to obtain the data if there is no log WS. + name: string; // Name of the item or page viewed. + url?: string; // Moodle URL. You can use the URL without the domain, e.g. /mod/foo/view.php. + data?: { + id?: number | string; // ID of the item viewed (if any). + category?: string; // Category of the data viewed (if any). + [key: string]: string | number | boolean | undefined; + }; +}; + +/** + * Data specific for the PUSH_NOTIFICATION events. + */ +export type CoreAnalyticsPushEvent = { + type: CoreAnalyticsEventType.PUSH_NOTIFICATION; + eventName: string; // Name of the event. + data: CorePushNotificationsNotificationBasicData; +}; + +/** + * Data specific for the DOWNLOAD_FILE events. + */ +export type CoreAnalyticsDownloadFileEvent = { + type: CoreAnalyticsEventType.DOWNLOAD_FILE; + fileUrl: string; +}; + +/** + * Data specific for the OPEN_LINK events. + */ +export type CoreAnalyticsOpenLinkEvent = { + type: CoreAnalyticsEventType.OPEN_LINK; + link: string; +}; diff --git a/src/core/services/filepool.ts b/src/core/services/filepool.ts index 3c633823aa9..6c352d77742 100644 --- a/src/core/services/filepool.ts +++ b/src/core/services/filepool.ts @@ -55,6 +55,7 @@ import { lazyMap, LazyMap } from '../utils/lazy-map'; import { asyncInstance, AsyncInstance } from '../utils/async-instance'; import { CorePath } from '@singletons/path'; import { CorePromisedValue } from '@classes/promised-value'; +import { CoreAnalytics, CoreAnalyticsEventType } from './analytics'; /* * Factory for handling downloading files and retrieve downloaded files. @@ -764,6 +765,11 @@ export class CoreFilepoolProvider { extension: fileEntry.extension, }); + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.DOWNLOAD_FILE, + fileUrl: CoreUrlUtils.unfixPluginfileURL(fileUrl, site.getURL()), + }); + // Add the anchor again to the local URL. return fileEntry.toURL() + (anchor || ''); }).finally(() => { diff --git a/src/core/services/tests/utils/url.test.ts b/src/core/services/tests/utils/url.test.ts index 50b3fea571c..fe3ee07b3be 100644 --- a/src/core/services/tests/utils/url.test.ts +++ b/src/core/services/tests/utils/url.test.ts @@ -65,6 +65,17 @@ describe('CoreUrlUtilsProvider', () => { expect(url).toEqual(originalUrl); }); + it('doesn\'t add undefined or null params', () => { + const originalUrl = 'https://moodle.org'; + const url = urlUtils.addParamsToUrl(originalUrl, { + foo: undefined, + bar: null, + baz: 1, + }); + + expect(url).toEqual('https://moodle.org?baz=1'); + }); + it('adds anchor to URL', () => { const originalUrl = 'https://moodle.org'; const params = { diff --git a/src/core/services/utils/url.ts b/src/core/services/utils/url.ts index f0148b84cf6..b0c1286eb9e 100644 --- a/src/core/services/utils/url.ts +++ b/src/core/services/utils/url.ts @@ -64,18 +64,18 @@ export class CoreUrlUtilsProvider { const urlAndAnchor = url.split('#'); url = urlAndAnchor[0]; - let separator = url.indexOf('?') != -1 ? '&' : '?'; + let separator = url.indexOf('?') !== -1 ? '&' : '?'; for (const key in params) { let value = params[key]; - if (boolToNumber && typeof value == 'boolean') { + if (boolToNumber && typeof value === 'boolean') { // Convert booleans to 1 or 0. value = value ? '1' : '0'; } - // Ignore objects. - if (typeof value != 'object') { + // Ignore objects and undefined. + if (typeof value !== 'object' && value !== undefined) { url += separator + key + '=' + value; separator = '&'; } @@ -542,7 +542,10 @@ export class CoreUrlUtilsProvider { return url; } - // Not a pluginfile URL. Treat webservice/pluginfile case. + // Check tokenpluginfile first. + url = url.replace(/\/tokenpluginfile\.php\/[^/]+\//, '/pluginfile.php/'); + + // Treat webservice/pluginfile case. url = url.replace(/\/webservice\/pluginfile\.php\//, '/pluginfile.php/'); // Make sure the URL doesn't contain the token. diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts index 4c10f5c40a0..a1e67503188 100644 --- a/src/core/services/utils/utils.ts +++ b/src/core/services/utils/utils.ts @@ -39,6 +39,8 @@ import { CoreErrorWithOptions } from '@classes/errors/errorwithoptions'; import { CoreFilepool } from '@services/filepool'; import { CoreSites } from '@services/sites'; import { CoreCancellablePromise } from '@classes/cancellable-promise'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreUrlUtils } from './url'; export type TreeNode = T & { children: TreeNode[] }; @@ -1058,7 +1060,7 @@ export class CoreUtilsProvider { * @param options Override default options passed to InAppBrowser. * @returns The opened window. */ - openInApp(url: string, options?: InAppBrowserOptions): InAppBrowserObject { + openInApp(url: string, options?: CoreUtilsOpenInAppOptions): InAppBrowserObject { options = options || {}; options.usewkwebview = 'yes'; // Force WKWebView in iOS. options.enableViewPortScale = options.enableViewPortScale ?? 'yes'; // Enable zoom on iOS by default. @@ -1116,6 +1118,11 @@ export class CoreUtilsProvider { }); } + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.OPEN_LINK, + link: CoreUrlUtils.unfixPluginfileURL(options.originalUrl ?? url), + }); + return this.iabInstance; } @@ -1170,14 +1177,20 @@ export class CoreUtilsProvider { * @param options Options. */ async openInBrowser(url: string, options: CoreUtilsOpenInBrowserOptions = {}): Promise { + const originaUrl = CoreUrlUtils.unfixPluginfileURL(options.originalUrl ?? options.browserWarningUrl ?? url); if (options.showBrowserWarning || options.showBrowserWarning === undefined) { try { - await CoreWindow.confirmOpenBrowserIfNeeded(options.browserWarningUrl ?? url); + await CoreWindow.confirmOpenBrowserIfNeeded(originaUrl); } catch (error) { return; // Cancelled, stop. } } + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.OPEN_LINK, + link: originaUrl, + }); + window.open(url, '_system'); } @@ -1204,12 +1217,19 @@ export class CoreUtilsProvider { type: mimetype, }; - return WebIntent.startActivity(options).catch((error) => { + try { + await WebIntent.startActivity(options); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.OPEN_LINK, + link: CoreUrlUtils.unfixPluginfileURL(url), + }); + } catch (error) { this.logger.error('Error opening online file ' + url + ' with mimetype ' + mimetype); this.logger.error('Error: ', JSON.stringify(error)); throw new Error(Translate.instant('core.erroropenfilenoapp')); - }); + } } // In the rest of platforms we need to open them in InAppBrowser. @@ -1897,7 +1917,18 @@ export type CoreUtilsOpenFileOptions = { */ export type CoreUtilsOpenInBrowserOptions = { showBrowserWarning?: boolean; // Whether to display a warning before opening in browser. Defaults to true. - browserWarningUrl?: string; // The URL to display in the warning message. Use it to hide sensitive information. + originalUrl?: string; // Original URL to open (in case the URL was treated, e.g. to add a token or an auto-login). + /** + * @deprecated since 4.3, use originalUrl instead. + */ + browserWarningUrl?: string; +}; + +/** + * Options for opening in InAppBrowser. + */ +export type CoreUtilsOpenInAppOptions = InAppBrowserOptions & { + originalUrl?: string; // Original URL to open (in case the URL was treated, e.g. to add a token or an auto-login). }; /** diff --git a/src/core/singletons/object.ts b/src/core/singletons/object.ts index e89b0e0ff74..a0e0ee4ac75 100644 --- a/src/core/singletons/object.ts +++ b/src/core/singletons/object.ts @@ -30,6 +30,20 @@ export type CoreObjectWithoutUndefined = Pretty<{ */ export class CoreObject { + /** + * Returns a value of an object and deletes it from the object. + * + * @param obj Object. + * @param key Key of the value to consume. + * @returns Whether objects are equal. + */ + static consumeKey(obj: T, key: K): T[K] { + const value = obj[key]; + delete obj[key]; + + return value; + } + /** * Check if two objects have the same shape and the same leaf values. * diff --git a/src/core/singletons/tests/object.test.ts b/src/core/singletons/tests/object.test.ts index c0ce0d302e8..64702ef104a 100644 --- a/src/core/singletons/tests/object.test.ts +++ b/src/core/singletons/tests/object.test.ts @@ -16,6 +16,22 @@ import { CoreObject } from '@singletons/object'; describe('CoreObject singleton', () => { + it('consumes object keys', () => { + const object = { + foo: 'a', + bar: 'b', + baz: 'c', + }; + + const fooValue = CoreObject.consumeKey(object, 'foo'); + const bazValue = CoreObject.consumeKey(object, 'baz'); + + expect(fooValue).toEqual('a'); + expect(bazValue).toEqual('c'); + expect(object.bar).toEqual('b'); + expect(Object.keys(object)).toEqual(['bar']); + }); + it('compares two values, checking all subproperties if needed', () => { expect(CoreObject.deepEquals(1, 1)).toBe(true); expect(CoreObject.deepEquals(1, 2)).toBe(false); diff --git a/src/types/config.d.ts b/src/types/config.d.ts index cd32bce2d57..49743638d2f 100644 --- a/src/types/config.d.ts +++ b/src/types/config.d.ts @@ -50,7 +50,6 @@ export interface EnvironmentConfig { forcedefaultlanguage: boolean; privacypolicy: string; notificoncolor: string; - enableanalytics: boolean; enableonboarding: boolean; forceColorScheme: CoreColorScheme; forceLoginLogo: boolean; diff --git a/upgrade.txt b/upgrade.txt index e0b022b497e..dcb5816f9a7 100644 --- a/upgrade.txt +++ b/upgrade.txt @@ -5,6 +5,8 @@ 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 ===