summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/lib/MomentsPageHub.jsm
blob: e37d1df8b16494639734a78443df0cb0831d5f31 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  setInterval: "resource://gre/modules/Timer.sys.mjs",
  clearInterval: "resource://gre/modules/Timer.sys.mjs",
});

// Frequency at which to check for new messages
const SYSTEM_TICK_INTERVAL = 5 * 60 * 1000;
const HOMEPAGE_OVERRIDE_PREF = "browser.startup.homepage_override.once";

// For the "reach" event of Messaging Experiments
const REACH_EVENT_CATEGORY = "messaging_experiments";
const REACH_EVENT_METHOD = "reach";
// Note it's not "moments-page" as Telemetry Events only accepts understores
// for the event `object`
const REACH_EVENT_OBJECT = "moments_page";

class _MomentsPageHub {
  constructor() {
    this.id = "moments-page-hub";
    this.state = {};
    this.checkHomepageOverridePref = this.checkHomepageOverridePref.bind(this);
    this._initialized = false;
  }

  async init(
    waitForInitialized,
    { handleMessageRequest, addImpression, blockMessageById, sendTelemetry }
  ) {
    if (this._initialized) {
      return;
    }

    this._initialized = true;
    this._handleMessageRequest = handleMessageRequest;
    this._addImpression = addImpression;
    this._blockMessageById = blockMessageById;
    this._sendTelemetry = sendTelemetry;

    // Need to wait for ASRouter to initialize before trying to fetch messages
    await waitForInitialized;

    this.messageRequest({
      triggerId: "momentsUpdate",
      template: "update_action",
    });

    const _intervalId = lazy.setInterval(
      () => this.checkHomepageOverridePref(),
      SYSTEM_TICK_INTERVAL
    );
    this.state = { _intervalId };
  }

  _sendPing(ping) {
    this._sendTelemetry({
      type: "MOMENTS_PAGE_TELEMETRY",
      data: { action: "moments_user_event", ...ping },
    });
  }

  sendUserEventTelemetry(message) {
    this._sendPing({
      message_id: message.id,
      bucket_id: message.id,
      event: "MOMENTS_PAGE_SET",
    });
  }

  /**
   * If we don't have `expire` defined with the message it could be because
   * it depends on user dependent parameters. Since the message matched
   * targeting we calculate `expire` based on the current timestamp and the
   * `expireDelta` which defines for how long it should be available.
   * @param expireDelta {number} - Offset in milliseconds from the current date
   */
  getExpirationDate(expireDelta) {
    return Date.now() + expireDelta;
  }

  executeAction(message) {
    const { id, data } = message.content.action;
    switch (id) {
      case "moments-wnp":
        const { url, expireDelta } = data;
        let { expire } = data;
        if (!expire) {
          expire = this.getExpirationDate(expireDelta);
        }
        // In order to reset this action we can dispatch a new message that
        // will overwrite the prev value with an expiration date from the past.
        Services.prefs.setStringPref(
          HOMEPAGE_OVERRIDE_PREF,
          JSON.stringify({ message_id: message.id, url, expire })
        );
        // Add impression and block immediately after taking the action
        this.sendUserEventTelemetry(message);
        this._addImpression(message);
        this._blockMessageById(message.id);
        break;
    }
  }

  _recordReachEvent(message) {
    const extra = { branches: message.branchSlug };
    Services.telemetry.recordEvent(
      REACH_EVENT_CATEGORY,
      REACH_EVENT_METHOD,
      REACH_EVENT_OBJECT,
      message.experimentSlug,
      extra
    );
  }

  async messageRequest({ triggerId, template }) {
    const telemetryObject = { triggerId };
    TelemetryStopwatch.start("MS_MESSAGE_REQUEST_TIME_MS", telemetryObject);
    const messages = await this._handleMessageRequest({
      triggerId,
      template,
      returnAll: true,
    });
    TelemetryStopwatch.finish("MS_MESSAGE_REQUEST_TIME_MS", telemetryObject);

    // Record the "reach" event for all the messages with `forReachEvent`,
    // only execute action for the first message without forReachEvent.
    const nonReachMessages = [];
    for (const message of messages) {
      if (message.forReachEvent) {
        if (!message.forReachEvent.sent) {
          this._recordReachEvent(message);
          message.forReachEvent.sent = true;
        }
      } else {
        nonReachMessages.push(message);
      }
    }
    if (nonReachMessages.length) {
      this.executeAction(nonReachMessages[0]);
    }
  }

  /**
   * Pref is set via Remote Settings message. We want to continously
   * monitor new messages that come in to ensure the one with the
   * highest priority is set.
   */
  checkHomepageOverridePref() {
    this.messageRequest({
      triggerId: "momentsUpdate",
      template: "update_action",
    });
  }

  uninit() {
    lazy.clearInterval(this.state._intervalId);
    this.state = {};
    this._initialized = false;
  }
}

/**
 * ToolbarBadgeHub - singleton instance of _ToolbarBadgeHub that can initiate
 * message requests and render messages.
 */
const MomentsPageHub = new _MomentsPageHub();

const EXPORTED_SYMBOLS = ["_MomentsPageHub", "MomentsPageHub"];