diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index ee550e2c1c5c..1267f849ee3a 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -147,6 +147,7 @@ android { if (!mozconfig.substs.MOZ_ANDROID_GCM) { exclude 'org/mozilla/gecko/gcm/**/*.java' exclude 'org/mozilla/gecko/push/**/*.java' + exclude 'org/mozilla/gecko/advertising/**' } if (!mozconfig.substs.MOZ_ANDROID_GOOGLE_PLAY_SERVICES) { @@ -177,6 +178,7 @@ android { if (!mozconfig.substs.MOZ_ANDROID_GCM) { exclude 'org/mozilla/gecko/gcm/**/*.java' exclude 'org/mozilla/gecko/push/**/*.java' + exclude 'org/mozilla/gecko/advertising/**' } } resources { @@ -244,6 +246,8 @@ dependencies { implementation "com.google.android.gms:play-services-basement:$google_play_services_version" implementation "com.google.android.gms:play-services-base:$google_play_services_version" implementation "com.google.android.gms:play-services-gcm:$google_play_services_version" + implementation "com.google.android.gms:play-services-ads-identifier:$google_play_services_version" + implementation "org.mindrot:jbcrypt:0.4" } if (mozconfig.substs.MOZ_ANDROID_GOOGLE_PLAY_SERVICES) { diff --git a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java index d2509e21913e..7887635073ad 100644 --- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java +++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java @@ -138,6 +138,7 @@ import org.mozilla.gecko.tabs.TabsPanel; import org.mozilla.gecko.telemetry.TelemetryCorePingDelegate; import org.mozilla.gecko.telemetry.TelemetryUploadService; import org.mozilla.gecko.telemetry.measurements.SearchCountMeasurements; +import org.mozilla.gecko.telemetry.TelemetryActivationPingDelegate; import org.mozilla.gecko.toolbar.AutocompleteHandler; import org.mozilla.gecko.toolbar.BrowserToolbar; import org.mozilla.gecko.toolbar.BrowserToolbar.CommitEventSource; @@ -317,6 +318,7 @@ public class BrowserApp extends GeckoApp private final DynamicToolbar mDynamicToolbar = new DynamicToolbar(); private final TelemetryCorePingDelegate mTelemetryCorePingDelegate = new TelemetryCorePingDelegate(); + private final TelemetryActivationPingDelegate mTelemetryActivationPingDelegate = new TelemetryActivationPingDelegate(); private final List delegates = Collections.unmodifiableList(Arrays.asList( new ScreenshotDelegate(), @@ -324,6 +326,7 @@ public class BrowserApp extends GeckoApp new ReaderViewBookmarkPromotion(), new PostUpdateHandler(), mTelemetryCorePingDelegate, + mTelemetryActivationPingDelegate, new OfflineTabStatusDelegate(), new AdjustBrowserAppDelegate(mTelemetryCorePingDelegate) )); diff --git a/mobile/android/base/java/org/mozilla/gecko/advertising/AdvertisingUtil.java b/mobile/android/base/java/org/mozilla/gecko/advertising/AdvertisingUtil.java new file mode 100644 index 000000000000..9c5e71028615 --- /dev/null +++ b/mobile/android/base/java/org/mozilla/gecko/advertising/AdvertisingUtil.java @@ -0,0 +1,35 @@ +package org.mozilla.gecko.advertising; + +import android.content.Context; +import android.util.Log; + +import com.google.android.gms.ads.identifier.AdvertisingIdClient; + + +import org.mindrot.jbcrypt.BCrypt; +import org.mozilla.gecko.annotation.ReflectionTarget; + +@ReflectionTarget +public class AdvertisingUtil { + private static final String LOG_TAG = AdvertisingUtil.class.getCanonicalName(); + + /* Use the same SALT for all BCrypt hashings. We want the SALT to be stable for all Fennec users but it should differ from the one from Fenix. + * Generated using Bcrypt.gensalt(). */ + private static final String BCRYPT_SALT = "$2a$10$ZfglUfcbmTyaBbAQ7SL9OO"; + + /** + * Retrieves the advertising ID hashed with BCrypt. Requires Google Play Services. Note: This method must not run on + * the main thread. + */ + @ReflectionTarget + public static String getAdvertisingId(Context caller) { + try { + AdvertisingIdClient.Info info = AdvertisingIdClient.getAdvertisingIdInfo(caller); + String advertisingId = info.getId(); + return advertisingId != null ? BCrypt.hashpw(advertisingId, BCRYPT_SALT) : null; + } catch (Throwable t) { + Log.e(LOG_TAG, "Error retrieving advertising ID. " + t.getMessage()); + } + return null; + } +} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryActivationPingDelegate.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryActivationPingDelegate.java new file mode 100644 index 000000000000..42e506d64531 --- /dev/null +++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryActivationPingDelegate.java @@ -0,0 +1,99 @@ +package org.mozilla.gecko.telemetry; + +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.WorkerThread; +import android.util.Log; + +import org.mozilla.gecko.AppConstants; +import org.mozilla.gecko.BrowserApp; +import org.mozilla.gecko.GeckoProfile; +import org.mozilla.gecko.GeckoThread; +import org.mozilla.gecko.delegates.BrowserAppDelegate; +import org.mozilla.gecko.telemetry.pingbuilders.TelemetryActivationPingBuilder; +import org.mozilla.gecko.util.StringUtils; +import org.mozilla.gecko.util.ThreadUtils; + +import java.io.IOException; +import java.lang.reflect.Method; + +/** + * An activity-lifecycle delegate for uploading the activation ping. + */ +public class TelemetryActivationPingDelegate extends BrowserAppDelegate { + private static final String LOGTAG = StringUtils.safeSubstring( + "Gecko" + TelemetryActivationPingDelegate.class.getSimpleName(), 0, 23); + + private TelemetryDispatcher telemetryDispatcher; // lazy + + + @Override + public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) { + super.onCreate(browserApp, savedInstanceState); + uploadActivationPing(browserApp); + } + + private void uploadActivationPing(final BrowserApp activity) { + if (!AppConstants.MOZ_ANDROID_GCM) { + return; + } + + if (TelemetryActivationPingBuilder.activationPingAlreadySent(activity)) { + return; + } + + ThreadUtils.postToBackgroundThread(() -> { + if (activity == null) { + return; + } + + if (!TelemetryUploadService.isUploadEnabledByAppConfig(activity)) { + Log.d(LOGTAG, "Activation ping upload disabled by app config. Returning."); + return; + } + + String identifier = null; + + try { + final Class clazz = Class.forName("org.mozilla.gecko.advertising.AdvertisingUtil"); + final Method getAdvertisingId = clazz.getMethod("getAdvertisingId", Context.class); + identifier = (String) getAdvertisingId.invoke(null, activity); + } catch (Exception e) { + Log.w(LOGTAG, "Unable to get identifier: " + e); + } + + final GeckoProfile profile = GeckoThread.getActiveProfile(); + String clientID = null; + try { + clientID = profile.getClientId(); + } catch (final IOException e) { + Log.w(LOGTAG, "Unable to get client ID: " + e); + if (identifier == null) { + //Activation ping is mandatory to be sent with either the identifier or the clientID. + Log.d(LOGTAG, "Activation ping failed to send - both identifier and clientID were unable to be retrieved."); + return; + } + } + + final TelemetryActivationPingBuilder pingBuilder = new TelemetryActivationPingBuilder(activity); + if (identifier != null) { + pingBuilder.setIdentifier(identifier); + } else { + pingBuilder.setClientID(clientID); + } + + getTelemetryDispatcher().queuePingForUpload(activity, pingBuilder); + }); + } + + @WorkerThread // via constructor + private TelemetryDispatcher getTelemetryDispatcher() { + if (telemetryDispatcher == null) { + final GeckoProfile profile = GeckoThread.getActiveProfile(); + final String profilePath = profile.getDir().getAbsolutePath(); + final String profileName = profile.getName(); + telemetryDispatcher = new TelemetryDispatcher(profilePath, profileName); + } + return telemetryDispatcher; + } +} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryCorePingDelegate.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryCorePingDelegate.java index e0ed965a1c79..020bf0cbca40 100644 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryCorePingDelegate.java +++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryCorePingDelegate.java @@ -21,11 +21,11 @@ import org.mozilla.gecko.GeckoThread; import org.mozilla.gecko.Telemetry; import org.mozilla.gecko.TelemetryContract; import org.mozilla.gecko.adjust.AttributionHelperListener; -import org.mozilla.gecko.telemetry.measurements.CampaignIdMeasurements; import org.mozilla.gecko.delegates.BrowserAppDelegateWithReference; import org.mozilla.gecko.distribution.DistributionStoreCallback; import org.mozilla.gecko.search.SearchEngineManager; import org.mozilla.gecko.sync.ExtendedJSONObject; +import org.mozilla.gecko.telemetry.measurements.CampaignIdMeasurements; import org.mozilla.gecko.telemetry.measurements.SearchCountMeasurements; import org.mozilla.gecko.telemetry.measurements.SessionMeasurements; import org.mozilla.gecko.telemetry.pingbuilders.TelemetryCorePingBuilder; @@ -139,47 +139,43 @@ public class TelemetryCorePingDelegate extends BrowserAppDelegateWithReference // the first launch of the activity doesn't trigger profile init too early. // // Additionally, getAndIncrementSequenceNumber must be called from a worker thread. - ThreadUtils.postToBackgroundThread(new Runnable() { - @WorkerThread - @Override - public void run() { - final BrowserApp activity = getBrowserApp(); - if (activity == null) { - return; - } - - final GeckoProfile profile = GeckoThread.getActiveProfile(); - if (!TelemetryUploadService.isUploadEnabledByProfileConfig(activity, profile)) { - Log.d(LOGTAG, "Core ping upload disabled by profile config. Returning."); - return; - } - - final String clientID; - final boolean hadCanaryClientId; - try { - clientID = profile.getClientId(); - hadCanaryClientId = profile.getIfHadCanaryClientId(); - } catch (final IOException e) { - Log.w(LOGTAG, "Unable to get client ID properties to generate core ping: " + e); - return; - } - - // Each profile can have different telemetry data so we intentionally grab the shared prefs for the profile. - final SharedPreferences sharedPrefs = getSharedPreferences(activity); - final SessionMeasurements.SessionMeasurementsContainer sessionMeasurementsContainer = - sessionMeasurements.getAndResetSessionMeasurements(activity); - final TelemetryCorePingBuilder pingBuilder = new TelemetryCorePingBuilder(activity) - .setClientID(clientID) - .setHadCanaryClientId(hadCanaryClientId) - .setDefaultSearchEngine(TelemetryCorePingBuilder.getEngineIdentifier(engine)) - .setProfileCreationDate(TelemetryCorePingBuilder.getProfileCreationDate(activity, profile)) - .setSequenceNumber(TelemetryCorePingBuilder.getAndIncrementSequenceNumber(sharedPrefs)) - .setSessionCount(sessionMeasurementsContainer.sessionCount) - .setSessionDuration(sessionMeasurementsContainer.elapsedSeconds); - maybeSetOptionalMeasurements(activity, sharedPrefs, pingBuilder); - - getTelemetryDispatcher(activity).queuePingForUpload(activity, pingBuilder); + ThreadUtils.postToBackgroundThread(() -> { + final BrowserApp activity = getBrowserApp(); + if (activity == null) { + return; } + + final GeckoProfile profile = GeckoThread.getActiveProfile(); + if (!TelemetryUploadService.isUploadEnabledByProfileConfig(activity, profile)) { + Log.d(LOGTAG, "Core ping upload disabled by profile config. Returning."); + return; + } + + final String clientID; + final boolean hadCanaryClientId; + try { + clientID = profile.getClientId(); + hadCanaryClientId = profile.getIfHadCanaryClientId(); + } catch (final IOException e) { + Log.w(LOGTAG, "Unable to get client ID properties to generate core ping: " + e); + return; + } + + // Each profile can have different telemetry data so we intentionally grab the shared prefs for the profile. + final SharedPreferences sharedPrefs = getSharedPreferences(activity); + final SessionMeasurements.SessionMeasurementsContainer sessionMeasurementsContainer = + sessionMeasurements.getAndResetSessionMeasurements(activity); + final TelemetryCorePingBuilder pingBuilder = new TelemetryCorePingBuilder(activity) + .setClientID(clientID) + .setHadCanaryClientId(hadCanaryClientId) + .setDefaultSearchEngine(TelemetryCorePingBuilder.getEngineIdentifier(engine)) + .setProfileCreationDate(TelemetryCorePingBuilder.getProfileCreationDate(activity, profile)) + .setSequenceNumber(TelemetryCorePingBuilder.getAndIncrementSequenceNumber(sharedPrefs)) + .setSessionCount(sessionMeasurementsContainer.sessionCount) + .setSessionDuration(sessionMeasurementsContainer.elapsedSeconds); + maybeSetOptionalMeasurements(activity, sharedPrefs, pingBuilder); + + getTelemetryDispatcher(activity).queuePingForUpload(activity, pingBuilder); }); } diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryDispatcher.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryDispatcher.java index d2303281eaf4..ba6797dfd895 100644 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryDispatcher.java +++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryDispatcher.java @@ -9,6 +9,8 @@ package org.mozilla.gecko.telemetry; import android.content.Context; import android.support.annotation.WorkerThread; import android.util.Log; + +import org.mozilla.gecko.telemetry.pingbuilders.TelemetryActivationPingBuilder; import org.mozilla.gecko.telemetry.pingbuilders.TelemetryCorePingBuilder; import org.mozilla.gecko.telemetry.pingbuilders.TelemetryCrashPingBuilder; import org.mozilla.gecko.telemetry.schedulers.TelemetryUploadScheduler; @@ -90,6 +92,14 @@ public class TelemetryDispatcher { queuePingForUpload(context, ping, coreStore, uploadAllPingsImmediatelyScheduler); } + /** + * Queues the given ping for upload and potentially schedules upload. This method can be called from any thread. + */ + public void queuePingForUpload(final Context context, final TelemetryActivationPingBuilder pingBuilder) { + final TelemetryOutgoingPing ping = pingBuilder.build(); + queuePingForUpload(context, ping, coreStore, uploadAllPingsImmediatelyScheduler); + } + /** * Queues the given crash ping for upload and potentially schedules upload. This method can be called from any thread. */ diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java index 1723816a30f5..816ab7e13645 100644 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java +++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java @@ -19,6 +19,7 @@ import org.mozilla.gecko.sync.ExtendedJSONObject; import org.mozilla.gecko.sync.net.BaseResource; import org.mozilla.gecko.sync.net.BaseResourceDelegate; import org.mozilla.gecko.sync.net.Resource; +import org.mozilla.gecko.telemetry.pingbuilders.TelemetryActivationPingBuilder; import org.mozilla.gecko.telemetry.stores.TelemetryPingStore; import org.mozilla.gecko.util.DateUtil; import org.mozilla.gecko.util.NetworkUtils; @@ -125,6 +126,8 @@ public class TelemetryUploadService extends JobIntentService { if (delegate.hadConnectionError()) { break; } + + checkPingsPersistence(context, ping.getDocID()); } final boolean wereAllUploadsSuccessful = !delegate.hadConnectionError(); @@ -136,6 +139,23 @@ public class TelemetryUploadService extends JobIntentService { return wereAllUploadsSuccessful; } + /** + * Check if we have any pings that need to persist their succesful upload status in order to prevent further attempts. + * E.g. {@link TelemetryActivationPingBuilder} + * @param context + */ + private static void checkPingsPersistence(Context context, String successfulUploadID) { + final String activationID = TelemetryActivationPingBuilder.getActivationPingId(context); + + if (activationID == null) { + return; + } + + if (activationID.equals(successfulUploadID)) { + TelemetryActivationPingBuilder.setActivationPingSent(context, true); + } + } + private static void uploadPayload(final String url, final ExtendedJSONObject payload, final ResultDelegate delegate) { final BaseResource resource; try { diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryActivationPingBuilder.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryActivationPingBuilder.java new file mode 100644 index 000000000000..bc6f5bb8a1f5 --- /dev/null +++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryActivationPingBuilder.java @@ -0,0 +1,128 @@ +package org.mozilla.gecko.telemetry.pingbuilders; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Build; +import android.support.annotation.NonNull; + +import org.mozilla.gecko.AppConstants; +import org.mozilla.gecko.GeckoSharedPrefs; +import org.mozilla.gecko.Locales; +import org.mozilla.gecko.distribution.DistributionStoreCallback; +import org.mozilla.gecko.telemetry.TelemetryOutgoingPing; +import org.mozilla.gecko.util.DateUtil; +import org.mozilla.gecko.util.StringUtils; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Locale; + +/** + * Builds a {@link TelemetryOutgoingPing} representing a activation ping. + * + */ +public class TelemetryActivationPingBuilder extends TelemetryPingBuilder { + private static final String LOGTAG = StringUtils.safeSubstring(TelemetryActivationPingBuilder.class.getSimpleName(), 0, 23); + + //Using MOZ_APP_BASENAME would be more elegant but according to the server side schema we need to be sure that we always send the "Fennec" value. + private static final String APP_NAME_VALUE = "Fennec"; + + private static final String PREFS_ACTIVATION_ID = "activation_ping_id"; + private static final String PREFS_ACTIVATION_SENT = "activation_ping_sent"; + + private static final String NAME = "activation"; + private static final int VERSION_VALUE = 1; + + private static final String IDENTIFIER = "identifier"; + private static final String CLIENT_ID = "clientId"; + private static final String MANUFACTURER = "manufacturer"; + private static final String MODEL = "model"; + private static final String DISTRIBUTION_ID = "distribution_id"; + private static final String LOCALE = "locale"; + private static final String OS_ATTR = "os"; + private static final String OS_VERSION = "osversion"; + private static final String PING_CREATION_DATE = "created"; + private static final String TIMEZONE_OFFSET = "tz"; + private static final String APP_NAME = "app_name"; + private static final String CHANNEL = "channel"; + + public TelemetryActivationPingBuilder(final Context context) { + super(VERSION_VALUE, true); + initPayloadConstants(context); + } + + private void initPayloadConstants(final Context context) { + payload.put(MANUFACTURER, Build.MANUFACTURER); + payload.put(MODEL, Build.MODEL); + payload.put(LOCALE, Locales.getLanguageTag(Locale.getDefault())); + payload.put(OS_ATTR, TelemetryPingBuilder.OS_NAME); + payload.put(OS_VERSION, Integer.toString(Build.VERSION.SDK_INT)); // A String for cross-platform reasons. + + final Calendar nowCalendar = Calendar.getInstance(); + final DateFormat pingCreationDateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.US); + payload.put(PING_CREATION_DATE, pingCreationDateFormat.format(nowCalendar.getTime())); + payload.put(TIMEZONE_OFFSET, DateUtil.getTimezoneOffsetInMinutesForGivenDate(nowCalendar)); + payload.put(APP_NAME, APP_NAME_VALUE); + payload.put(CHANNEL, AppConstants.ANDROID_PACKAGE_NAME); + + SharedPreferences prefs = GeckoSharedPrefs.forApp(context); + final String distributionId = prefs.getString(DistributionStoreCallback.PREF_DISTRIBUTION_ID, null); + if (distributionId != null) { + payload.put(DISTRIBUTION_ID, distributionId); + } + + prefs.edit().putString(PREFS_ACTIVATION_ID, docID).apply(); + } + + public static boolean activationPingAlreadySent(Context context) { + return GeckoSharedPrefs.forApp(context).getBoolean(PREFS_ACTIVATION_SENT, false); + } + + public static void setActivationPingSent(Context context, boolean value) { + SharedPreferences prefs = GeckoSharedPrefs.forApp(context); + prefs.edit().putBoolean(PREFS_ACTIVATION_SENT, value).apply(); + prefs.edit().remove(PREFS_ACTIVATION_ID).apply(); + } + + public static String getActivationPingId(Context context) { + return GeckoSharedPrefs.forApp(context).getString(PREFS_ACTIVATION_ID, null); + } + + @Override + public String getDocType() { + return NAME; + } + + @Override + public String[] getMandatoryFields() { + return new String[] { + MANUFACTURER, + MODEL, + LOCALE, + OS_ATTR, + OS_VERSION, + PING_CREATION_DATE, + TIMEZONE_OFFSET, + APP_NAME, + CHANNEL + }; + } + + public TelemetryActivationPingBuilder setIdentifier(@NonNull final String identifier) { + if (identifier == null) { + throw new IllegalArgumentException("Expected non-null identifier"); + } + + payload.put(IDENTIFIER, identifier); + return this; + } + + public TelemetryActivationPingBuilder setClientID(@NonNull final String clientID) { + if (clientID == null) { + throw new IllegalArgumentException("Expected non-null clientID"); + } + payload.put(CLIENT_ID, clientID); + return this; + } +} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryPingBuilder.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryPingBuilder.java index 590b9b7ecd50..0295aba31942 100644 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryPingBuilder.java +++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryPingBuilder.java @@ -25,6 +25,9 @@ abstract class TelemetryPingBuilder { // In the server url, the initial path directly after the "scheme://host:port/" private static final String SERVER_INITIAL_PATH = "submit/telemetry"; + // Modern pings now use a structured ingestion where we capture the schema version as one of the URI parameters. + private static final String SERVER_INITIAL_PATH_MODERN = "submit/mobile"; + // By default Fennec ping's use the old telemetry version, this can be overridden private static final int DEFAULT_TELEMETRY_VERSION = 1; @@ -48,6 +51,12 @@ abstract class TelemetryPingBuilder { payload = new ExtendedJSONObject(); } + public TelemetryPingBuilder(int version, boolean modernPing) { + docID = UUID.randomUUID().toString(); + serverPath = modernPing ? getModernTelemetryServerPath(getDocType(), docID, version) : getTelemetryServerPath(getDocType(), docID, version); + payload = new ExtendedJSONObject(); + } + /** * @return the name of the ping (e.g. "core") */ @@ -100,4 +109,22 @@ abstract class TelemetryPingBuilder { appBuildId + (version == UNIFIED_TELEMETRY_VERSION ? "?v=4" : ""); } + + /** + * Returns a url of the format: + * http://hostname/submit/mobile/docType/appVersion/docId/ + * + * User for modern structured ingestion. + * + * @param docType The name of the ping (e.g. "main") + * @param docID A UUID that identifies the ping + * @param version The ping format version + * @return a url at which to POST the telemetry data to + */ + private static String getModernTelemetryServerPath(final String docType, final String docID, int version) { + return SERVER_INITIAL_PATH_MODERN + '/' + + docType + '/' + + version + '/' + + docID; + } }