Skip to main content

headless_lms_utils/
page_visit_hasher.rs

1use uuid::Uuid;
2
3use crate::stable_digest::stable_digest;
4
5/// A visitor identifier that cannot be traced back to the visitor once the day's key is rotated
6/// away.
7///
8/// `hashing_key_for_the_day` is what keeps the identifier from being reproducible from an IP
9/// address alone, so it must be the current day's secret and never a constant. The same visitor
10/// gets a different identifier on the next day and on another course.
11///
12/// Touching how the parts are combined re-keys every identifier the moment it deploys, so
13/// `COUNT(DISTINCT anonymous_identifier)` counts every visitor already seen that day a second time.
14/// The key rotation caps that to the day of the deploy; nothing else does.
15pub fn hash_anonymous_identifier(
16    course_id: Uuid,
17    hashing_key_for_the_day: &[u8],
18    user_agent: &str,
19    ip_address: &str,
20) -> String {
21    stable_digest(&[
22        course_id.as_bytes(),
23        hashing_key_for_the_day,
24        ip_address.as_bytes(),
25        user_agent.as_bytes(),
26    ])
27}
28
29#[cfg(test)]
30mod tests {
31    use super::*;
32
33    /// The identifier is the join key for a course's visit counts, so two visitors that differ only
34    /// in where one field ends and the next begins have to be told apart. Unframed fields run
35    /// together and count that pair as one visitor.
36    #[test]
37    fn visitors_whose_fields_run_together_get_different_identifiers() {
38        let course_id = Uuid::new_v4();
39        let key = vec![1, 2, 3];
40
41        assert_ne!(
42            hash_anonymous_identifier(course_id, &key, "Firefox", "10.0.0.1"),
43            hash_anonymous_identifier(course_id, &key, "efox", "10.0.0.1Fir"),
44        );
45    }
46}