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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
// 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 https://mozilla.org/MPL/2.0/.

use std::fmt;
use std::sync::Arc;

use crate::ping::PingMaker;
use crate::upload::PingPayload;
use crate::Glean;

use uuid::Uuid;

/// Stores information about a ping.
///
/// This is required so that given metric data queued on disk we can send
/// pings with the correct settings, e.g. whether it has a client_id.
#[derive(Clone)]
pub struct PingType(Arc<InnerPing>);

struct InnerPing {
    /// The name of the ping.
    pub name: String,
    /// Whether the ping should include the client ID.
    pub include_client_id: bool,
    /// Whether the ping should be sent if it is empty
    pub send_if_empty: bool,
    /// Whether to use millisecond-precise start/end times.
    pub precise_timestamps: bool,
    /// Whether to include the {client|ping}_info sections on assembly.
    pub include_info_sections: bool,
    /// Whether this ping is enabled.
    pub enabled: bool,
    /// Other pings that should be scheduled when this ping is sent.
    pub schedules_pings: Vec<String>,
    /// The "reason" codes that this ping can send
    pub reason_codes: Vec<String>,
}

impl fmt::Debug for PingType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("PingType")
            .field("name", &self.0.name)
            .field("include_client_id", &self.0.include_client_id)
            .field("send_if_empty", &self.0.send_if_empty)
            .field("precise_timestamps", &self.0.precise_timestamps)
            .field("include_info_sections", &self.0.include_info_sections)
            .field("enabled", &self.0.enabled)
            .field("schedules_pings", &self.0.schedules_pings)
            .field("reason_codes", &self.0.reason_codes)
            .finish()
    }
}

// IMPORTANT:
//
// When changing this implementation, make sure all the operations are
// also declared in the related trait in `../traits/`.
impl PingType {
    /// Creates a new ping type for the given name, whether to include the client ID and whether to
    /// send this ping empty.
    ///
    /// # Arguments
    ///
    /// * `name` - The name of the ping.
    /// * `include_client_id` - Whether to include the client ID in the assembled ping when submitting.
    /// * `send_if_empty` - Whether the ping should be sent empty or not.
    /// * `precise_timestamps` - Whether the ping should use precise timestamps for the start and end time.
    /// * `include_info_sections` - Whether the ping should include the client/ping_info sections.
    /// * `enabled` - Whether or not this ping is enabled. Note: Data that would be sent on a disabled
    ///   ping will still be collected but is discarded rather than being submitted.
    /// * `reason_codes` - The valid reason codes for this ping.
    #[allow(clippy::too_many_arguments)]
    pub fn new<A: Into<String>>(
        name: A,
        include_client_id: bool,
        send_if_empty: bool,
        precise_timestamps: bool,
        include_info_sections: bool,
        enabled: bool,
        schedules_pings: Vec<String>,
        reason_codes: Vec<String>,
    ) -> Self {
        Self::new_internal(
            name,
            include_client_id,
            send_if_empty,
            precise_timestamps,
            include_info_sections,
            enabled,
            schedules_pings,
            reason_codes,
        )
    }

    #[allow(clippy::too_many_arguments)]
    pub(crate) fn new_internal<A: Into<String>>(
        name: A,
        include_client_id: bool,
        send_if_empty: bool,
        precise_timestamps: bool,
        include_info_sections: bool,
        enabled: bool,
        schedules_pings: Vec<String>,
        reason_codes: Vec<String>,
    ) -> Self {
        let this = Self(Arc::new(InnerPing {
            name: name.into(),
            include_client_id,
            send_if_empty,
            precise_timestamps,
            include_info_sections,
            enabled,
            schedules_pings,
            reason_codes,
        }));

        // Register this ping.
        // That will happen asynchronously and not block operation.
        crate::register_ping_type(&this);

        this
    }

    pub(crate) fn name(&self) -> &str {
        &self.0.name
    }

    pub(crate) fn include_client_id(&self) -> bool {
        self.0.include_client_id
    }

    pub(crate) fn send_if_empty(&self) -> bool {
        self.0.send_if_empty
    }

    pub(crate) fn precise_timestamps(&self) -> bool {
        self.0.precise_timestamps
    }

    pub(crate) fn include_info_sections(&self) -> bool {
        self.0.include_info_sections
    }

    pub(crate) fn enabled(&self, glean: &Glean) -> bool {
        let remote_settings_config = &glean.remote_settings_config.lock().unwrap();

        if !remote_settings_config.pings_enabled.is_empty() {
            if let Some(remote_enabled) = remote_settings_config.pings_enabled.get(self.name()) {
                return *remote_enabled;
            }
        }

        self.0.enabled
    }

    pub(crate) fn schedules_pings(&self) -> &[String] {
        &self.0.schedules_pings
    }

    /// Submits the ping for eventual uploading.
    ///
    /// The ping content is assembled as soon as possible, but upload is not
    /// guaranteed to happen immediately, as that depends on the upload policies.
    ///
    /// If the ping currently contains no content, it will not be sent,
    /// unless it is configured to be sent if empty.
    ///
    /// # Arguments
    ///
    /// * `reason` - the reason the ping was triggered. Included in the
    ///   `ping_info.reason` part of the payload.
    pub fn submit(&self, reason: Option<String>) {
        let ping = PingType(Arc::clone(&self.0));

        // Need to separate access to the Glean object from access to global state.
        // `trigger_upload` itself might lock the Glean object and we need to avoid that deadlock.
        crate::dispatcher::launch(|| {
            let sent =
                crate::core::with_glean(move |glean| ping.submit_sync(glean, reason.as_deref()));
            if sent {
                let state = crate::global_state().lock().unwrap();
                if let Err(e) = state.callbacks.trigger_upload() {
                    log::error!("Triggering upload failed. Error: {}", e);
                }
            }
        })
    }

    /// Collects and submits a ping for eventual uploading.
    ///
    /// # Returns
    ///
    /// Whether the ping was succesfully assembled and queued.
    #[doc(hidden)]
    pub fn submit_sync(&self, glean: &Glean, reason: Option<&str>) -> bool {
        if !glean.is_upload_enabled() {
            log::info!("Glean disabled: not submitting any pings.");
            return false;
        }

        let ping = &self.0;

        // Allowing `clippy::manual_filter`.
        // This causes a false positive.
        // We have a side-effect in the `else` branch,
        // so shouldn't delete it.
        #[allow(unknown_lints)]
        #[allow(clippy::manual_filter)]
        let corrected_reason = match reason {
            Some(reason) => {
                if ping.reason_codes.contains(&reason.to_string()) {
                    Some(reason)
                } else {
                    log::error!("Invalid reason code {} for ping {}", reason, ping.name);
                    None
                }
            }
            None => None,
        };

        let ping_maker = PingMaker::new();
        let doc_id = Uuid::new_v4().to_string();
        let url_path = glean.make_path(&ping.name, &doc_id);
        match ping_maker.collect(glean, self, corrected_reason, &doc_id, &url_path) {
            None => {
                log::info!(
                    "No content for ping '{}', therefore no ping queued.",
                    ping.name
                );
                false
            }
            Some(ping) => {
                if !self.enabled(glean) {
                    log::info!(
                        "The ping '{}' is disabled and will be discarded and not submitted",
                        ping.name
                    );

                    return false;
                }

                // This metric is recorded *after* the ping is collected (since
                // that is the only way to know *if* it will be submitted). The
                // implication of this is that the count for a metrics ping will
                // be included in the *next* metrics ping.
                glean
                    .additional_metrics
                    .pings_submitted
                    .get(ping.name)
                    .add_sync(glean, 1);

                if let Err(e) = ping_maker.store_ping(glean.get_data_path(), &ping) {
                    log::warn!(
                        "IO error while writing ping to file: {}. Enqueuing upload of what we have in memory.",
                        e
                    );
                    glean.additional_metrics.io_errors.add_sync(glean, 1);
                    // `serde_json::to_string` only fails if serialization of the content
                    // fails or it contains maps with non-string keys.
                    // However `ping.content` is already a `JsonValue`,
                    // so both scenarios should be impossible.
                    let content =
                        ::serde_json::to_string(&ping.content).expect("ping serialization failed");
                    // TODO: Shouldn't we consolidate on a single collected Ping representation?
                    let ping = PingPayload {
                        document_id: ping.doc_id.to_string(),
                        upload_path: ping.url_path.to_string(),
                        json_body: content,
                        headers: Some(ping.headers),
                        body_has_info_sections: self.0.include_info_sections,
                        ping_name: self.0.name.to_string(),
                    };

                    glean.upload_manager.enqueue_ping(glean, ping);
                    return true;
                }

                glean.upload_manager.enqueue_ping_from_file(glean, &doc_id);

                log::info!(
                    "The ping '{}' was submitted and will be sent as soon as possible",
                    ping.name
                );

                if !ping.schedules_pings.is_empty() {
                    log::info!(
                        "The ping '{}' is being used to schedule other pings: {:?}",
                        ping.name,
                        ping.schedules_pings
                    );
                    for scheduled_ping_name in &ping.schedules_pings {
                        glean.submit_ping_by_name(scheduled_ping_name, reason);
                    }
                }

                true
            }
        }
    }
}