From a4d0c17efd24106449b7e750bd4acf928870c230 Mon Sep 17 00:00:00 2001 From: Jon Chambers Date: Fri, 11 Dec 2020 13:59:50 -0500 Subject: [PATCH] Record OS versions for iOS requests. --- .../metrics/MetricsRequestEventListener.java | 40 +++++++++++++++++++ .../MetricsRequestEventListenerTest.java | 29 ++++++++++++++ .../util/ua/UserAgentUtilTest.java | 1 + 3 files changed, 70 insertions(+) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/metrics/MetricsRequestEventListener.java b/service/src/main/java/org/whispersystems/textsecuregcm/metrics/MetricsRequestEventListener.java index 8e0e1786d..381515989 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/metrics/MetricsRequestEventListener.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/metrics/MetricsRequestEventListener.java @@ -7,6 +7,8 @@ package org.whispersystems.textsecuregcm.metrics; import com.codahale.metrics.MetricRegistry; import com.google.common.annotations.VisibleForTesting; +import com.vdurmont.semver4j.Semver; +import com.vdurmont.semver4j.SemverException; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Metrics; import io.micrometer.core.instrument.Tag; @@ -21,6 +23,8 @@ import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil; import java.util.ArrayList; import java.util.List; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Gathers and reports request-level metrics. @@ -34,6 +38,7 @@ class MetricsRequestEventListener implements RequestEventListener { static final String ANDROID_REQUEST_COUNTER_NAME = MetricRegistry.name(MetricsRequestEventListener.class, "androidRequest"); static final String DESKTOP_REQUEST_COUNTER_NAME = MetricRegistry.name(MetricsRequestEventListener.class, "desktopRequest"); + static final String IOS_REQUEST_COUNTER_NAME = MetricRegistry.name(MetricsRequestEventListener.class, "iosRequest"); static final String OS_TAG = "os"; static final String SDK_TAG = "sdkVersion"; @@ -43,6 +48,11 @@ class MetricsRequestEventListener implements RequestEventListener { private static final int MIN_ANDROID_SDK_VERSION = 19; private static final int MAX_ANDROID_SDK_VERSION = 50; + private static final String IOS_VERSION_PREFIX = "iOS/"; + private static final Pattern LEGACY_IOS_PATTERN = Pattern.compile("^\\(.*iOS ([0-9\\.]+).*\\)$"); + private static final Semver MIN_IOS_VERSION = new Semver("8.0", Semver.SemverType.LOOSE); + private static final Semver MAX_IOS_VERSION = new Semver("20.0", Semver.SemverType.LOOSE); + private final TrafficSource trafficSource; private final MeterRegistry meterRegistry; @@ -75,6 +85,7 @@ class MetricsRequestEventListener implements RequestEventListener { recordDesktopOperatingSystem(userAgent); recordAndroidSdkVersion(userAgent); + recordIosVersion(userAgent); } catch (final UnrecognizedUserAgentException ignored) { } } @@ -108,6 +119,35 @@ class MetricsRequestEventListener implements RequestEventListener { } } + @VisibleForTesting + void recordIosVersion(final UserAgent userAgent) { + if (userAgent.getPlatform() == ClientPlatform.IOS) { + userAgent.getAdditionalSpecifiers().ifPresent(additionalSpecifiers -> { + Semver iosVersion = null; + + if (additionalSpecifiers.startsWith(IOS_VERSION_PREFIX)) { + try { + iosVersion = new Semver(additionalSpecifiers.substring(IOS_VERSION_PREFIX.length()), Semver.SemverType.LOOSE); + } catch (final SemverException ignored) { + } + } else { + final Matcher matcher = LEGACY_IOS_PATTERN.matcher(additionalSpecifiers); + + if (matcher.matches()) { + try { + iosVersion = new Semver(matcher.group(1), Semver.SemverType.LOOSE); + } catch (final SemverException ignored) { + } + } + } + + if (iosVersion != null && iosVersion.isGreaterThanOrEqualTo(MIN_IOS_VERSION) && iosVersion.isLowerThan(MAX_IOS_VERSION)) { + meterRegistry.counter(IOS_REQUEST_COUNTER_NAME, OS_TAG, iosVersion.toString()).increment(); + } + }); + } + } + @VisibleForTesting static String getPathTemplate(final ExtendedUriInfo uriInfo) { final StringBuilder pathBuilder = new StringBuilder(); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/metrics/MetricsRequestEventListenerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/metrics/MetricsRequestEventListenerTest.java index 5f768fff9..c91691a17 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/metrics/MetricsRequestEventListenerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/metrics/MetricsRequestEventListenerTest.java @@ -302,6 +302,35 @@ public class MetricsRequestEventListenerTest { }; } + @Test + @Parameters(method = "argumentsForTestRecordIosVersion") + public void testRecordIosVersion(final UserAgent userAgent, final String expectedIosVersion) { + when(meterRegistry.counter(eq(MetricsRequestEventListener.IOS_REQUEST_COUNTER_NAME), (String)any())).thenReturn(counter); + listener.recordIosVersion(userAgent); + + if (expectedIosVersion != null) { + final ArgumentCaptor tagCaptor = ArgumentCaptor.forClass(String.class); + verify(meterRegistry).counter(eq(MetricsRequestEventListener.IOS_REQUEST_COUNTER_NAME), tagCaptor.capture()); + + assertEquals(List.of(MetricsRequestEventListener.OS_TAG, expectedIosVersion), tagCaptor.getAllValues()); + } else { + verify(meterRegistry, never()).counter(eq(MetricsRequestEventListener.IOS_REQUEST_COUNTER_NAME)); + verify(meterRegistry, never()).counter(eq(MetricsRequestEventListener.IOS_REQUEST_COUNTER_NAME), (String)any()); + } + } + + private static Object argumentsForTestRecordIosVersion() { + return new Object[] { + new Object[] { new UserAgent(ClientPlatform.IOS, new Semver("3.9.0"), "iOS/14.2"), "14.2" }, + new Object[] { new UserAgent(ClientPlatform.IOS, new Semver("3.9.0"), "(iPhone; iOS 12.2; Scale/3.00)"), "12.2" }, + new Object[] { new UserAgent(ClientPlatform.IOS, new Semver("3.9.0")), null }, + new Object[] { new UserAgent(ClientPlatform.IOS, new Semver("3.9.0"), "iOS/bogus"), null }, + new Object[] { new UserAgent(ClientPlatform.IOS, new Semver("3.9.0"), "(iPhone; iOS bogus; Scale/3.00)"), null }, + new Object[] { new UserAgent(ClientPlatform.ANDROID, new Semver("4.68.3"), "Android/25"), null }, + new Object[] { new UserAgent(ClientPlatform.DESKTOP, new Semver("1.2.3"), "Linux"), null } + }; + } + private static SubProtocol.WebSocketResponseMessage getResponse(ArgumentCaptor responseCaptor) throws InvalidProtocolBufferException { return SubProtocol.WebSocketMessage.parseFrom(responseCaptor.getValue().array()).getResponse(); } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/util/ua/UserAgentUtilTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/util/ua/UserAgentUtilTest.java index 17cb2b310..4731305b9 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/util/ua/UserAgentUtilTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/util/ua/UserAgentUtilTest.java @@ -61,6 +61,7 @@ public class UserAgentUtilTest { new Object[] { "Signal-Desktop/1.2.3", new UserAgent(ClientPlatform.DESKTOP, new Semver("1.2.3")) }, new Object[] { "Signal-Desktop/1.32.0-beta.3", new UserAgent(ClientPlatform.DESKTOP, new Semver("1.32.0-beta.3")) }, new Object[] { "Signal-iOS/3.9.0 (iPhone; iOS 12.2; Scale/3.00)", new UserAgent(ClientPlatform.IOS, new Semver("3.9.0"), "(iPhone; iOS 12.2; Scale/3.00)") }, + new Object[] { "Signal-iOS/3.9.0 iOS/14.2", new UserAgent(ClientPlatform.IOS, new Semver("3.9.0"), "iOS/14.2") }, new Object[] { "Signal-iOS/3.9.0", new UserAgent(ClientPlatform.IOS, new Semver("3.9.0")) } }; }