File: study_filtering.cc

package info (click to toggle)
chromium 139.0.7258.127-1
  • links: PTS, VCS
  • area: main
  • in suites:
  • size: 6,122,068 kB
  • sloc: cpp: 35,100,771; ansic: 7,163,530; javascript: 4,103,002; python: 1,436,920; asm: 946,517; xml: 746,709; pascal: 187,653; perl: 88,691; sh: 88,436; objc: 79,953; sql: 51,488; cs: 44,583; fortran: 24,137; makefile: 22,147; tcl: 15,277; php: 13,980; yacc: 8,984; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (447 lines) | stat: -rw-r--r-- 15,565 bytes parent folder | download | duplicates (3)
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
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/variations/study_filtering.h"

#include <stddef.h>
#include <stdint.h>

#include <cstdint>
#include <functional>
#include <set>
#include <string_view>

#include "base/containers/contains.h"
#include "base/logging.h"
#include "base/strings/string_util.h"
#include "components/variations/variations_layers.h"
#include "components/variations/variations_seed_processor.h"

namespace variations {
namespace {

// Converts |date_time| in Study date format to base::Time.
base::Time ConvertStudyDateToBaseTime(int64_t date_time) {
  return base::Time::UnixEpoch() + base::Seconds(date_time);
}

// Similar to base::Contains(), but specifically for ASCII strings and
// case-insensitive comparison.
template <typename Collection>
bool ContainsStringIgnoreCaseASCII(const Collection& collection,
                                   const std::string& value) {
  return std::ranges::any_of(collection, [&value](const std::string& s) {
    return base::EqualsCaseInsensitiveASCII(s, value);
  });
}

}  // namespace

namespace internal {

bool CheckStudyChannel(const Study::Filter& filter, Study::Channel channel) {
  // An empty channel list matches all channels.
  if (filter.channel_size() == 0)
    return true;

  return base::Contains(filter.channel(), channel);
}

bool CheckStudyFormFactor(const Study::Filter& filter,
                          Study::FormFactor form_factor) {
  // If both filters are empty, match all values.
  if (filter.form_factor_size() == 0 && filter.exclude_form_factor_size() == 0)
    return true;

  // Allow the |form_factor| if it's in the allowlist.
  // Note if both are specified, the excludelist is ignored. We do not expect
  // both to be present for Chrome due to server-side checks.
  if (filter.form_factor_size() > 0)
    return base::Contains(filter.form_factor(), form_factor);

  // Omit if there is a matching excludelist entry.
  return !base::Contains(filter.exclude_form_factor(), form_factor);
}

bool CheckStudyCpuArchitecture(const Study::Filter& filter,
                               Study::CpuArchitecture cpu_architecture) {
  // If both filters are empty, match all values.
  if (filter.cpu_architecture_size() == 0 &&
      filter.exclude_cpu_architecture_size() == 0) {
    return true;
  }

  // Allow the |cpu_architecture| if it's in the allowlist.
  // Note if both are specified, the excludelist is ignored. We do not expect
  // both to be present for Chrome due to server-side checks.
  if (filter.cpu_architecture_size() > 0)
    return base::Contains(filter.cpu_architecture(), cpu_architecture);

  // Omit if there is a matching excludelist entry.
  return !base::Contains(filter.exclude_cpu_architecture(), cpu_architecture);
}

bool CheckStudyHardwareClass(const Study::Filter& filter,
                             const std::string& hardware_class) {
  // If both filters are empty, match all values.
  if (filter.hardware_class_size() == 0 &&
      filter.exclude_hardware_class_size() == 0) {
    return true;
  }

  // Note: This logic changed in M66. Prior to M66, this used substring
  // comparison logic to match hardware classes. In M66, it was made consistent
  // with other filters.

  // Allow the |hardware_class| if it's in the allowlist.
  // Note if both are specified, the excludelist is ignored. We do not expect
  // both to be present for Chrome due to server-side checks.
  if (filter.hardware_class_size() > 0) {
    return ContainsStringIgnoreCaseASCII(filter.hardware_class(),
                                         hardware_class);
  }

  // Omit if there is a matching excludelist entry.
  return !ContainsStringIgnoreCaseASCII(filter.exclude_hardware_class(),
                                        hardware_class);
}

bool CheckStudyLocale(const Study::Filter& filter, const std::string& locale) {
  // If both filters are empty, match all values.
  if (filter.locale_size() == 0 && filter.exclude_locale_size() == 0)
    return true;

  // Allow the |locale| if it's in the allowlist.
  // Note if both are specified, the excludelist is ignored. We do not expect
  // both to be present for Chrome due to server-side checks.
  if (filter.locale_size() > 0)
    return base::Contains(filter.locale(), locale);

  // Omit if there is a matching excludelist entry.
  return !base::Contains(filter.exclude_locale(), locale);
}

bool CheckStudyCountry(const Study::Filter& filter,
                       const std::string& country) {
  // If both filters are empty, match all values.
  if (filter.country_size() == 0 && filter.exclude_country_size() == 0)
    return true;

  // Allow the |country| if it's in the allowlist.
  // Note if both are specified, the excludelist is ignored. We do not expect
  // both to be present for Chrome due to server-side checks.
  if (filter.country_size() > 0)
    return base::Contains(filter.country(), country);

  // Omit if there is a matching excludelist entry.
  return !base::Contains(filter.exclude_country(), country);
}

bool CheckStudyPlatform(const Study::Filter& filter, Study::Platform platform) {
  return base::Contains(filter.platform(), platform);
}

bool CheckStudyLowEndDevice(const Study::Filter& filter,
                            bool is_low_end_device) {
  return !filter.has_is_low_end_device() ||
         filter.is_low_end_device() == is_low_end_device;
}

bool CheckStudyPolicyRestriction(const Study::Filter& filter,
                                 RestrictionPolicy policy_restriction) {
  switch (policy_restriction) {
    // If the policy is set to no restrictions let any study that is not
    // specifically designated for clients requesting critical studies only.
    case RestrictionPolicy::NO_RESTRICTIONS:
      return filter.policy_restriction() != Study::CRITICAL_ONLY;
    // If the policy is set to only allow critical studies than make sure they
    // have that restriction applied on their Filter.
    case RestrictionPolicy::CRITICAL_ONLY:
      return filter.policy_restriction() != Study::NONE;
    // If the policy is set to not allow any variations then return false
    // regardless of the actual Filter.
    case RestrictionPolicy::ALL:
      return false;
  }
}

bool CheckStudyStartDate(const Study::Filter& filter,
                         const base::Time& date_time) {
  if (filter.has_start_date()) {
    const base::Time start_date =
        ConvertStudyDateToBaseTime(filter.start_date());
    return date_time >= start_date;
  }

  return true;
}

bool CheckStudyEndDate(const Study::Filter& filter,
                       const base::Time& date_time) {
  if (filter.has_end_date()) {
    const base::Time end_date = ConvertStudyDateToBaseTime(filter.end_date());
    return end_date >= date_time;
  }

  return true;
}

bool CheckStudyVersion(const Study::Filter& filter,
                       const base::Version& version) {
  if (filter.has_min_version()) {
    if (version.CompareToWildcardString(filter.min_version()) < 0)
      return false;
  }

  if (filter.has_max_version()) {
    if (version.CompareToWildcardString(filter.max_version()) > 0)
      return false;
  }

  return true;
}

bool CheckStudyOSVersion(const Study::Filter& filter,
                         const base::Version& version) {
  if (filter.has_min_os_version()) {
    if (!version.IsValid() ||
        version.CompareToWildcardString(filter.min_os_version()) < 0) {
      return false;
    }
  }

  if (filter.has_max_os_version()) {
    if (!version.IsValid() ||
        version.CompareToWildcardString(filter.max_os_version()) > 0) {
      return false;
    }
  }

  return true;
}

bool CheckStudyEnterprise(const Study::Filter& filter,
                          const ClientFilterableState& client_state) {
  return !filter.has_is_enterprise() ||
         filter.is_enterprise() == client_state.IsEnterprise();
}

bool CheckStudyGoogleGroup(const Study::Filter& filter,
                           const ClientFilterableState& client_state) {
  if (filter.google_group_size() == 0 &&
      filter.exclude_google_group_size() == 0) {
    // This study doesn't have any google group configuration, so break early.
    return true;
  }

  // Fetch the groups this client is a member of.
  base::flat_set<uint64_t> client_groups = client_state.GoogleGroups();

  if (filter.google_group_size() > 0) {
    if (std::ranges::none_of(filter.google_group(),
                             [&client_groups](int64_t group) {
                               return base::Contains(client_groups, group);
                             })) {
      // A google_group filter was specified, and the client is not a member of
      // any of the groups.
      return false;
    }
  }

  if (filter.exclude_google_group_size() > 0) {
    if (std::ranges::any_of(filter.exclude_google_group(),
                            [&client_groups](int64_t group) {
                              return base::Contains(client_groups, group);
                            })) {
      // An exclude_google_group filter was specified, and the client is a
      // member of at least one of the groups.
      return false;
    }
  }

  return true;
}

const std::string& GetClientCountryForStudy(
    const Study& study,
    const ClientFilterableState& client_state) {
  switch (study.consistency()) {
    case Study::SESSION:
      return client_state.session_consistency_country;
    case Study::PERMANENT:
      // Use the saved country for permanent consistency studies. This allows
      // Chrome to use the same country for filtering permanent consistency
      // studies between Chrome upgrades. Since some studies have user-visible
      // effects, this helps to avoid annoying users with experimental group
      // churn while traveling.
      return client_state.permanent_consistency_country;
  }

  // Unless otherwise specified, use an empty country that won't pass any
  // filters that specifically include countries, but will pass any filters
  // that specifically exclude countries.
  return base::EmptyString();
}

bool ShouldAddStudy(const ProcessedStudy& processed_study,
                    const ClientFilterableState& client_state,
                    const VariationsLayers& layers) {
  const Study& study = *processed_study.study();
  if (study.has_expiry_date()) {
    DVLOG(1) << "Filtered out study " << study.name()
             << " due to unsupported expiry_date field.";
    return false;
  }

  if (study.has_layer()) {
    if (!layers.IsLayerMemberActive(study.layer())) {
      DVLOG(1) << "Filtered out study " << study.name()
               << " due to layer member not being active.";
      return false;
    }

    if (!VariationsLayers::AllowsHighEntropy(study) &&
        layers.ActiveLayerMemberDependsOnHighEntropy(
            study.layer().layer_id())) {
      DVLOG(1)
          << "Filtered out study " << study.name()
          << " due to not allowing a high entropy source yet being a member "
             "of a layer using the default (high) entropy source.";
      return false;
    }
  }

  if (study.has_filter()) {
    if (!CheckStudyChannel(study.filter(), client_state.channel)) {
      DVLOG(1) << "Filtered out study " << study.name() << " due to channel.";
      return false;
    }

    if (!CheckStudyFormFactor(study.filter(), client_state.form_factor)) {
      DVLOG(1) << "Filtered out study " << study.name() <<
                  " due to form factor.";
      return false;
    }

    if (!CheckStudyCpuArchitecture(study.filter(),
                                   client_state.cpu_architecture)) {
      DVLOG(1) << "Filtered out study " << study.name()
               << " due to cpu architecture.";
      return false;
    }

    if (!CheckStudyLocale(study.filter(), client_state.locale)) {
      DVLOG(1) << "Filtered out study " << study.name() << " due to locale.";
      return false;
    }

    if (!CheckStudyPlatform(study.filter(), client_state.platform)) {
      DVLOG(1) << "Filtered out study " << study.name() << " due to platform.";
      return false;
    }

    if (!CheckStudyVersion(study.filter(), client_state.version)) {
      DVLOG(1) << "Filtered out study " << study.name() << " due to version.";
      return false;
    }

    if (!CheckStudyStartDate(study.filter(), client_state.reference_date)) {
      DVLOG(1) << "Filtered out study " << study.name() <<
                  " due to start date.";
      return false;
    }

    if (!CheckStudyEndDate(study.filter(), client_state.reference_date)) {
      DVLOG(1) << "Filtered out study " << study.name() << " due to end date.";
      return false;
    }

    if (!CheckStudyHardwareClass(study.filter(), client_state.hardware_class)) {
      DVLOG(1) << "Filtered out study " << study.name() <<
                  " due to hardware_class.";
      return false;
    }

    if (!CheckStudyLowEndDevice(study.filter(),
                                client_state.is_low_end_device)) {
      DVLOG(1) << "Filtered out study " << study.name()
               << " due to is_low_end_device.";
      return false;
    }

    if (!CheckStudyPolicyRestriction(study.filter(),
                                     client_state.policy_restriction)) {
      DVLOG(1) << "Filtered out study " << study.name()
               << " due to policy restriction.";
      return false;
    }

    if (!CheckStudyOSVersion(study.filter(), client_state.os_version)) {
      DVLOG(1) << "Filtered out study " << study.name()
               << " due to os_version.";
      return false;
    }

    const std::string& country = GetClientCountryForStudy(study, client_state);
    if (!CheckStudyCountry(study.filter(), country)) {
      DVLOG(1) << "Filtered out study " << study.name() << " due to country.";
      return false;
    }

    // Check for enterprise status last as checking whether the client is
    // enterprise can be slow.
    if (!CheckStudyEnterprise(study.filter(), client_state)) {
      DVLOG(1) << "Filtered out study " << study.name()
               << " due to enterprise state.";
      return false;
    }

    if (!CheckStudyGoogleGroup(study.filter(), client_state)) {
      DVLOG(1) << "Filtered out study " << study.name()
               << " due to Google groups membership checks.";
      return false;
    }
  }

  DVLOG(1) << "Kept study " << study.name() << ".";
  return true;
}

}  // namespace internal

std::vector<ProcessedStudy> FilterAndValidateStudies(
    const VariationsSeed& seed,
    const ClientFilterableState& client_state,
    const VariationsLayers& layers) {
  DCHECK(client_state.version.IsValid());

  std::vector<ProcessedStudy> filtered_studies;

  // Don't create two studies with the same name.
  // These `string_view`s contain pointers which point to memory owned by
  // `seed`.
  std::set<std::string_view, std::less<>> created_studies;

  for (const Study& study : seed.study()) {
    ProcessedStudy processed_study;
    if (!processed_study.Init(&study))
      continue;

    if (!internal::ShouldAddStudy(processed_study, client_state, layers))
      continue;

    auto [it, inserted] =
        created_studies.insert(processed_study.study()->name());
    if (!inserted) {
      // The study's name is already in `created_studies`, which means that a
      // study with the same name was already added to `filtered_studies`.
      continue;
    }

    filtered_studies.push_back(processed_study);
  }
  return filtered_studies;
}

}  // namespace variations