Celebrate the diversity of UA strings when generating tags for metrics.

This commit is contained in:
Jon Chambers 2020-05-27 15:45:15 -04:00 committed by Jon Chambers
parent 9ba5ee8043
commit 06c82ee87d
2 changed files with 78 additions and 23 deletions

View File

@ -5,6 +5,7 @@ import org.whispersystems.textsecuregcm.util.Pair;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -14,39 +15,73 @@ import java.util.regex.Pattern;
*/
public class UserAgentTagUtil {
static final int MAX_VERSIONS = 10_000;
static final int MAX_VERSIONS = 10_000;
public static final String PLATFORM_TAG = "platform";
public static final String VERSION_TAG = "clientVersion";
public static final String PLATFORM_TAG = "platform";
public static final String VERSION_TAG = "clientVersion";
static final List<Tag> OVERFLOW_TAGS = List.of(Tag.of(PLATFORM_TAG, "overflow"), Tag.of(VERSION_TAG, "overflow"));
static final List<Tag> UNRECOGNIZED_TAGS = List.of(Tag.of(PLATFORM_TAG, "unrecognized"), Tag.of(VERSION_TAG, "unrecognized"));
static final List<Tag> OVERFLOW_TAGS = List.of(Tag.of(PLATFORM_TAG, "overflow"), Tag.of(VERSION_TAG, "overflow"));
static final List<Tag> UNRECOGNIZED_TAGS = List.of(Tag.of(PLATFORM_TAG, "unrecognized"), Tag.of(VERSION_TAG, "unrecognized"));
private static final Pattern USER_AGENT_PATTERN = Pattern.compile("Signal-([^ ]+) ([^ ]+).*$", Pattern.CASE_INSENSITIVE);
private static final Pattern USER_AGENT_PATTERN = Pattern.compile("^Signal[ \\-]([^ ]+) ([^ ]+).*$", Pattern.CASE_INSENSITIVE);
private static final Pattern IOS_USER_AGENT_PATTERN = Pattern.compile("^Signal/([^ ]+) \\(.*ios.*\\)$", Pattern.CASE_INSENSITIVE);
private static final Set<Pair<String, String>> SEEN_VERSIONS = new HashSet<>();
private static final Set<Pair<String, String>> SEEN_VERSIONS = new HashSet<>();
private UserAgentTagUtil() {
}
public static List<Tag> getUserAgentTags(final String userAgent) {
final Matcher matcher = USER_AGENT_PATTERN.matcher(userAgent);
final List<Tag> tags;
if (matcher.matches()) {
final Pair<String, String> platformAndVersion = new Pair<>(matcher.group(1).toLowerCase(), matcher.group(2));
final boolean allowVersion;
synchronized (SEEN_VERSIONS) {
allowVersion = SEEN_VERSIONS.contains(platformAndVersion) || (SEEN_VERSIONS.size() < MAX_VERSIONS && SEEN_VERSIONS.add(platformAndVersion));
}
tags = allowVersion ? List.of(Tag.of(PLATFORM_TAG, platformAndVersion.first()), Tag.of(VERSION_TAG, platformAndVersion.second())) : OVERFLOW_TAGS;
} else {
if (userAgent == null) {
tags = UNRECOGNIZED_TAGS;
} else {
tags = getAndroidOrDesktopUserAgentTags(userAgent)
.orElseGet(() -> getIOSUserAgentTags(userAgent)
.orElse(UNRECOGNIZED_TAGS));
}
return tags;
}
private static Optional<List<Tag>> getAndroidOrDesktopUserAgentTags(final String userAgent) {
final Matcher matcher = USER_AGENT_PATTERN.matcher(userAgent);
final Optional<List<Tag>> maybeTags;
if (matcher.matches()) {
final String platform = matcher.group(1).toLowerCase();
final String version = matcher.group(2);
maybeTags = Optional.of(allowVersion(platform, version) ? List.of(Tag.of(PLATFORM_TAG, platform), Tag.of(VERSION_TAG, version)) : OVERFLOW_TAGS);
} else {
maybeTags = Optional.empty();
}
return maybeTags;
}
private static Optional<List<Tag>> getIOSUserAgentTags(final String userAgent) {
final Matcher matcher = IOS_USER_AGENT_PATTERN.matcher(userAgent);
final Optional<List<Tag>> maybeTags;
if (matcher.matches()) {
final String platform = "ios";
final String version = matcher.group(1);
maybeTags = Optional.of(allowVersion(platform, version) ? List.of(Tag.of(PLATFORM_TAG, platform), Tag.of(VERSION_TAG, version)) : OVERFLOW_TAGS);
} else {
maybeTags = Optional.empty();
}
return maybeTags;
}
private static boolean allowVersion(final String platform, final String version) {
final Pair<String, String> platformAndVersion = new Pair<>(platform, version);
synchronized (SEEN_VERSIONS) {
return SEEN_VERSIONS.contains(platformAndVersion) || (SEEN_VERSIONS.size() < MAX_VERSIONS && SEEN_VERSIONS.add(platformAndVersion));
}
}
}

View File

@ -14,11 +14,31 @@ public class UserAgentTagUtilTest {
assertEquals(UserAgentTagUtil.UNRECOGNIZED_TAGS,
UserAgentTagUtil.getUserAgentTags("This is obviously not a reasonable User-Agent string."));
final List<Tag> tags = UserAgentTagUtil.getUserAgentTags("Signal-Android 4.53.7 (Android 8.1)");
assertEquals(UserAgentTagUtil.UNRECOGNIZED_TAGS, UserAgentTagUtil.getUserAgentTags(null));
assertEquals(2, tags.size());
assertTrue(tags.contains(Tag.of(UserAgentTagUtil.PLATFORM_TAG, "android")));
assertTrue(tags.contains(Tag.of(UserAgentTagUtil.VERSION_TAG, "4.53.7")));
{
final List<Tag> tags = UserAgentTagUtil.getUserAgentTags("Signal-Android 4.53.7 (Android 8.1)");
assertEquals(2, tags.size());
assertTrue(tags.contains(Tag.of(UserAgentTagUtil.PLATFORM_TAG, "android")));
assertTrue(tags.contains(Tag.of(UserAgentTagUtil.VERSION_TAG, "4.53.7")));
}
{
final List<Tag> tags = UserAgentTagUtil.getUserAgentTags("Signal Desktop 1.2.3");
assertEquals(2, tags.size());
assertTrue(tags.contains(Tag.of(UserAgentTagUtil.PLATFORM_TAG, "desktop")));
assertTrue(tags.contains(Tag.of(UserAgentTagUtil.VERSION_TAG, "1.2.3")));
}
{
final List<Tag> tags = UserAgentTagUtil.getUserAgentTags("Signal/3.9.0 (iPhone; iOS 12.2; Scale/3.00)");
assertEquals(2, tags.size());
assertTrue(tags.contains(Tag.of(UserAgentTagUtil.PLATFORM_TAG, "ios")));
assertTrue(tags.contains(Tag.of(UserAgentTagUtil.VERSION_TAG, "3.9.0")));
}
}
@Test