From 06c82ee87d752d354e4aa18ac85d88693d19d747 Mon Sep 17 00:00:00 2001 From: Jon Chambers Date: Wed, 27 May 2020 15:45:15 -0400 Subject: [PATCH] Celebrate the diversity of UA strings when generating tags for metrics. --- .../metrics/UserAgentTagUtil.java | 73 ++++++++++++++----- .../metrics/UserAgentTagUtilTest.java | 28 ++++++- 2 files changed, 78 insertions(+), 23 deletions(-) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/metrics/UserAgentTagUtil.java b/service/src/main/java/org/whispersystems/textsecuregcm/metrics/UserAgentTagUtil.java index 31c479752..aa4c91850 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/metrics/UserAgentTagUtil.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/metrics/UserAgentTagUtil.java @@ -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 OVERFLOW_TAGS = List.of(Tag.of(PLATFORM_TAG, "overflow"), Tag.of(VERSION_TAG, "overflow")); - static final List UNRECOGNIZED_TAGS = List.of(Tag.of(PLATFORM_TAG, "unrecognized"), Tag.of(VERSION_TAG, "unrecognized")); + static final List OVERFLOW_TAGS = List.of(Tag.of(PLATFORM_TAG, "overflow"), Tag.of(VERSION_TAG, "overflow")); + static final List 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> SEEN_VERSIONS = new HashSet<>(); + private static final Set> SEEN_VERSIONS = new HashSet<>(); private UserAgentTagUtil() { } public static List getUserAgentTags(final String userAgent) { - final Matcher matcher = USER_AGENT_PATTERN.matcher(userAgent); final List tags; - if (matcher.matches()) { - final Pair 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> getAndroidOrDesktopUserAgentTags(final String userAgent) { + final Matcher matcher = USER_AGENT_PATTERN.matcher(userAgent); + final Optional> 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> getIOSUserAgentTags(final String userAgent) { + final Matcher matcher = IOS_USER_AGENT_PATTERN.matcher(userAgent); + final Optional> 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 platformAndVersion = new Pair<>(platform, version); + + synchronized (SEEN_VERSIONS) { + return SEEN_VERSIONS.contains(platformAndVersion) || (SEEN_VERSIONS.size() < MAX_VERSIONS && SEEN_VERSIONS.add(platformAndVersion)); + } + } } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/metrics/UserAgentTagUtilTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/metrics/UserAgentTagUtilTest.java index e84b74194..f9545648a 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/metrics/UserAgentTagUtilTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/metrics/UserAgentTagUtilTest.java @@ -14,11 +14,31 @@ public class UserAgentTagUtilTest { assertEquals(UserAgentTagUtil.UNRECOGNIZED_TAGS, UserAgentTagUtil.getUserAgentTags("This is obviously not a reasonable User-Agent string.")); - final List 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 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 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 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