From 63e5c577dbb95c047f4b8dbcf8679e8298b101b8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 21:36:21 +0000 Subject: [PATCH 1/4] Initial plan From 27deaad36ccebbeea8b5cefd8853f103fce6e3e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 21:43:11 +0000 Subject: [PATCH 2/4] Fixes AB#3584247: Add telemetry schema data model and core interfaces (PBI-1) Agent-Logs-Url: https://github.com/AzureAD/microsoft-authentication-library-common-for-android/sessions/09541e92-6c54-4fd8-beab-81cca1bfae45 Co-authored-by: Prvnkmr337 <198076477+Prvnkmr337@users.noreply.github.com> --- .../common/java/AuthenticationConstants.java | 10 + .../java/broker/telemetry/EventCollector.kt | 75 ++++++ .../common/java/broker/telemetry/EventTag.kt | 62 +++++ .../java/broker/telemetry/ExecutionEvent.kt | 42 ++++ .../telemetry/ITelemetrySchemaProvider.kt | 31 +++ .../broker/telemetry/PerformanceRecord.kt | 40 +++ .../java/broker/telemetry/TelemetryHelper.kt | 47 ++++ .../java/broker/telemetry/TelemetrySchema.kt | 59 +++++ .../parameters/CommandParameters.java | 5 + .../common/java/exception/BaseException.java | 18 +- .../java/result/AcquireTokenResult.java | 18 +- .../broker/telemetry/EventCollectorTest.kt | 165 ++++++++++++ .../TelemetrySchemaSerializationTest.kt | 237 ++++++++++++++++++ 13 files changed, 807 insertions(+), 2 deletions(-) create mode 100644 common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/EventCollector.kt create mode 100644 common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/EventTag.kt create mode 100644 common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/ExecutionEvent.kt create mode 100644 common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/ITelemetrySchemaProvider.kt create mode 100644 common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/PerformanceRecord.kt create mode 100644 common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/TelemetryHelper.kt create mode 100644 common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/TelemetrySchema.kt create mode 100644 common4j/src/test/com/microsoft/identity/common/java/broker/telemetry/EventCollectorTest.kt create mode 100644 common4j/src/test/com/microsoft/identity/common/java/broker/telemetry/TelemetrySchemaSerializationTest.kt diff --git a/common4j/src/main/com/microsoft/identity/common/java/AuthenticationConstants.java b/common4j/src/main/com/microsoft/identity/common/java/AuthenticationConstants.java index dec771b7c9..e4efd16f7f 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/AuthenticationConstants.java +++ b/common4j/src/main/com/microsoft/identity/common/java/AuthenticationConstants.java @@ -599,6 +599,16 @@ public static final class Broker { */ public static final String BROKER_SILENT_EXECUTOR_POOL_SIZE = "broker_silent_executor_pool_size"; + /** + * Bundle key for the serialized broker telemetry request. + */ + public static final String BROKER_TELEMETRY_REQUEST = "broker_telemetry_request"; + + /** + * Bundle key for the serialized broker telemetry schema. + */ + public static final String BROKER_TELEMETRY_SCHEMA = "broker_telemetry_schema"; + /** * Broker application version name. */ diff --git a/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/EventCollector.kt b/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/EventCollector.kt new file mode 100644 index 0000000000..63d2072005 --- /dev/null +++ b/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/EventCollector.kt @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.common.java.broker.telemetry + +import java.time.Instant +import java.util.concurrent.CopyOnWriteArrayList + +/** + * Thread-safe accumulator for [ExecutionEvent]s during an authentication flow. + * + * Create one instance per authentication request and call [addEvent] at each phase boundary. + * When the flow completes, call [toTelemetrySchema] to obtain the structured result. + * + * @param correlationId The correlation ID of the authentication request being tracked. + */ +class EventCollector(private val correlationId: String) { + private val events = CopyOnWriteArrayList() + private val startTimeMs: Long = System.currentTimeMillis() + + /** + * Records a new [ExecutionEvent] with the current elapsed time. + * + * @param tag The [EventTag] identifying this phase. + * @param diagnosticCode Optional diagnostic code for additional context. + * @param errorCode Optional error code if this event represents a failure. + */ + fun addEvent(tag: EventTag, diagnosticCode: Int? = null, errorCode: Int? = null) { + events.add( + ExecutionEvent( + tag = tag, + timestampMs = System.currentTimeMillis() - startTimeMs, + diagnosticCode = diagnosticCode, + errorCode = errorCode + ) + ) + } + + /** + * Builds a [TelemetrySchema] from all events collected so far. + * The returned schema captures the total elapsed duration and the full event timeline. + * + * @return A [TelemetrySchema] containing a [PerformanceRecord] with all collected events. + */ + fun toTelemetrySchema(): TelemetrySchema { + val duration = System.currentTimeMillis() - startTimeMs + return TelemetrySchema( + correlationId = correlationId, + performanceRecord = PerformanceRecord( + startTime = Instant.ofEpochMilli(startTimeMs).toString(), + duration = duration, + executionFlow = events.toList() + ) + ) + } +} diff --git a/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/EventTag.kt b/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/EventTag.kt new file mode 100644 index 0000000000..a8219e734c --- /dev/null +++ b/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/EventTag.kt @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.common.java.broker.telemetry + +import com.google.gson.annotations.SerializedName + +/** + * Enum of event tags representing phases of the broker authentication flow. + * Each tag has a compact serialized value to minimize payload size. + */ +enum class EventTag(@SerializedName("v") val value: String) { + // BrokerEntry (5) + BrokerRequestReceived("bre.recv"), + BrokerRequestDeserialized("bre.dser"), + BrokerAccountLookupStart("bre.als"), + BrokerAccountLookupEnd("bre.ale"), + BrokerRequestValidated("bre.val"), + // BrokerDispatch (3) + BrokerControllerSelected("bdi.csel"), + BrokerCommandQueued("bdi.cq"), + BrokerCommandExecutionStart("bdi.cex"), + // BrokerCache (6) + BrokerCacheCheckStart("bca.cks"), + BrokerCacheCheckEnd("bca.cke"), + BrokerCacheHit("bca.hit"), + BrokerCacheMiss("bca.miss"), + BrokerCacheWriteStart("bca.wrs"), + BrokerCacheWriteEnd("bca.wre"), + // BrokerNetwork (5) + BrokerPrtLoadStart("bne.prt"), + BrokerNetworkCallStart("bne.ncs"), + BrokerNetworkCallEnd("bne.nce"), + BrokerTokenAcquired("bne.tok"), + BrokerNetworkCallFailed("bne.nfl"), + // BrokerResponse (3) + BrokerResponseSerialized("brs.ser"), + BrokerResponseSent("brs.snt"), + BrokerRequestFailed("brs.fail"), + // CommonStrategy (2) + CommonHttpRequestExecute("cst.hreq"), + CommonHttpResponseReceived("cst.hrsp") +} diff --git a/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/ExecutionEvent.kt b/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/ExecutionEvent.kt new file mode 100644 index 0000000000..dda71e4b11 --- /dev/null +++ b/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/ExecutionEvent.kt @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.common.java.broker.telemetry + +import com.google.gson.annotations.SerializedName + +/** + * Represents a single timed event within the broker authentication flow. + * + * @param tag The [EventTag] identifying which phase this event captures. + * @param timestampMs Elapsed time in milliseconds from the start of the flow. + * @param threadId The ID of the thread that recorded this event. + * @param diagnosticCode Optional diagnostic code for additional context. + * @param errorCode Optional error code if the event represents a failure. + */ +data class ExecutionEvent( + @SerializedName("t") val tag: EventTag, + @SerializedName("ts") val timestampMs: Long, + @SerializedName("tid") val threadId: Long = Thread.currentThread().id, + @SerializedName("d") val diagnosticCode: Int? = null, + @SerializedName("e") val errorCode: Int? = null +) diff --git a/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/ITelemetrySchemaProvider.kt b/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/ITelemetrySchemaProvider.kt new file mode 100644 index 0000000000..6815278cab --- /dev/null +++ b/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/ITelemetrySchemaProvider.kt @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.common.java.broker.telemetry + +/** + * Implemented by objects that can carry a [TelemetrySchema]. + * Mainly used to pass the structured broker telemetry between broker and client. + */ +interface ITelemetrySchemaProvider { + var telemetrySchema: TelemetrySchema? +} diff --git a/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/PerformanceRecord.kt b/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/PerformanceRecord.kt new file mode 100644 index 0000000000..e683ebe2ed --- /dev/null +++ b/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/PerformanceRecord.kt @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.common.java.broker.telemetry + +import com.google.gson.annotations.SerializedName + +/** + * Aggregation container for the performance data captured during an authentication flow. + * + * @param version Schema version for this record. Defaults to [TelemetrySchema.CURRENT_VERSION]. + * @param startTime ISO 8601 UTC timestamp when the flow started. + * @param duration Total elapsed time in milliseconds for the authentication flow. + * @param executionFlow Ordered list of [ExecutionEvent]s captured during the flow. + */ +data class PerformanceRecord( + @SerializedName("version") val version: String = TelemetrySchema.CURRENT_VERSION, + @SerializedName("start_time") val startTime: String, + @SerializedName("duration") val duration: Long, + @SerializedName("execution_flow") val executionFlow: List +) diff --git a/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/TelemetryHelper.kt b/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/TelemetryHelper.kt new file mode 100644 index 0000000000..15e5a1e059 --- /dev/null +++ b/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/TelemetryHelper.kt @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.common.java.broker.telemetry + +/** + * Null-safe utility for recording events into an [EventCollector]. + * Callers may hold a nullable collector reference and use this helper to avoid null checks. + */ +object TelemetryHelper { + /** + * Adds an event to the given [collector] if it is non-null; otherwise does nothing. + * + * @param collector The [EventCollector] to record the event into, or null. + * @param tag The [EventTag] identifying the phase. + * @param diagnosticCode Optional diagnostic code for additional context. + * @param errorCode Optional error code if this event represents a failure. + */ + @JvmStatic + fun addEventSafely( + collector: EventCollector?, + tag: EventTag, + diagnosticCode: Int? = null, + errorCode: Int? = null + ) { + collector?.addEvent(tag, diagnosticCode, errorCode) + } +} diff --git a/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/TelemetrySchema.kt b/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/TelemetrySchema.kt new file mode 100644 index 0000000000..b43b90e7b5 --- /dev/null +++ b/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/TelemetrySchema.kt @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.common.java.broker.telemetry + +import com.google.gson.annotations.SerializedName + +/** + * Top-level telemetry schema emitted by the broker for a single authentication request. + * Captures metadata, auth outcome, and a structured performance record. + * + * @param schemaVersion Version of this schema format. Defaults to [CURRENT_VERSION]. + * @param correlationId Correlation ID linking this schema to the authentication request. + * @param name Optional name of the authentication operation. + * @param version Optional version string of the caller SDK. + * @param authOutcome Optional outcome of the authentication (e.g., "success", "failure"). + * @param errorCode Optional error code if the authentication failed. + * @param responseStarvationDuration Optional duration in milliseconds the client waited + * without a response. + * @param powerPolicy Optional flag indicating whether power policy restrictions were active. + * @param deviceIdle Optional flag indicating whether the device was in idle mode. + * @param performanceRecord Optional [PerformanceRecord] containing the detailed event timeline. + */ +data class TelemetrySchema( + @SerializedName("schema_version") val schemaVersion: String = CURRENT_VERSION, + @SerializedName("correlation_id") val correlationId: String, + @SerializedName("name") val name: String? = null, + @SerializedName("version") val version: String? = null, + @SerializedName("auth_outcome") val authOutcome: String? = null, + @SerializedName("error_code") val errorCode: String? = null, + @SerializedName("response_starvation_duration") val responseStarvationDuration: Int? = null, + @SerializedName("power_policy") val powerPolicy: Boolean? = null, + @SerializedName("device_idle") val deviceIdle: Boolean? = null, + @SerializedName("perf") val performanceRecord: PerformanceRecord? = null +) { + companion object { + const val CURRENT_VERSION = "1.0.0" + const val MIN_SUPPORTED_MAJOR = 1 + } +} diff --git a/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/CommandParameters.java b/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/CommandParameters.java index 31680df323..c2919cd38a 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/CommandParameters.java +++ b/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/CommandParameters.java @@ -24,6 +24,7 @@ import com.google.gson.annotations.Expose; import com.microsoft.identity.common.java.WarningType; +import com.microsoft.identity.common.java.broker.telemetry.EventCollector; import com.microsoft.identity.common.java.interfaces.IPlatformComponents; import com.microsoft.identity.common.java.logging.Logger; import com.microsoft.identity.common.java.opentelemetry.SerializableSpanContext; @@ -61,6 +62,10 @@ public class CommandParameters { @EqualsAndHashCode.Exclude private transient boolean isSharedDevice; + @Setter + @EqualsAndHashCode.Exclude + private transient EventCollector eventCollector; + @Expose() private String applicationName; diff --git a/common4j/src/main/com/microsoft/identity/common/java/exception/BaseException.java b/common4j/src/main/com/microsoft/identity/common/java/exception/BaseException.java index be58d7e019..9b4a97d2f3 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/exception/BaseException.java +++ b/common4j/src/main/com/microsoft/identity/common/java/exception/BaseException.java @@ -29,6 +29,8 @@ import com.microsoft.identity.common.java.util.StringUtil; import com.microsoft.identity.common.java.broker.BrokerPerformanceMetrics; import com.microsoft.identity.common.java.broker.IBrokerPerformanceMetricsProvider; +import com.microsoft.identity.common.java.broker.telemetry.ITelemetrySchemaProvider; +import com.microsoft.identity.common.java.broker.telemetry.TelemetrySchema; import java.util.ArrayList; import java.util.Arrays; @@ -41,7 +43,7 @@ import lombok.NonNull; import lombok.experimental.Accessors; -public class BaseException extends Exception implements IErrorInformation, ITelemetryAccessor, IBrokerPerformanceMetricsProvider, IBrokerInfoProvider { +public class BaseException extends Exception implements IErrorInformation, ITelemetryAccessor, IBrokerPerformanceMetricsProvider, IBrokerInfoProvider, ITelemetrySchemaProvider { // This is needed for backward compatibility with older versions of MSAL (pre 3.0.0) // When MSAL converts the result bundle it looks for this value to know about exception type @@ -90,6 +92,9 @@ public class BaseException extends Exception implements IErrorInformation, ITele private BrokerPerformanceMetrics mBrokerPerformanceMetrics; + @Nullable + private TelemetrySchema mTelemetrySchema; + /** * {@link Exception#addSuppressed(Throwable)} requires API19 in Android, so we're creating our own. */ @@ -236,6 +241,17 @@ public BrokerPerformanceMetrics getBrokerPerformanceMetrics() { return this.mBrokerPerformanceMetrics; } + @Override + public void setTelemetrySchema(@Nullable final TelemetrySchema telemetrySchema) { + this.mTelemetrySchema = telemetrySchema; + } + + @Override + @Nullable + public TelemetrySchema getTelemetrySchema() { + return this.mTelemetrySchema; + } + public void setUsername(@Nullable final String username) { this.mUsername = username; } diff --git a/common4j/src/main/com/microsoft/identity/common/java/result/AcquireTokenResult.java b/common4j/src/main/com/microsoft/identity/common/java/result/AcquireTokenResult.java index 28fd66a2a3..de5d200fe8 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/result/AcquireTokenResult.java +++ b/common4j/src/main/com/microsoft/identity/common/java/result/AcquireTokenResult.java @@ -28,10 +28,12 @@ import com.microsoft.identity.common.java.providers.oauth2.AuthorizationResult; import com.microsoft.identity.common.java.broker.BrokerPerformanceMetrics; import com.microsoft.identity.common.java.broker.IBrokerPerformanceMetricsProvider; +import com.microsoft.identity.common.java.broker.telemetry.ITelemetrySchemaProvider; +import com.microsoft.identity.common.java.broker.telemetry.TelemetrySchema; import javax.annotation.Nullable; -public class AcquireTokenResult implements IBrokerPerformanceMetricsProvider, IBrokerInfoProvider { +public class AcquireTokenResult implements IBrokerPerformanceMetricsProvider, IBrokerInfoProvider, ITelemetrySchemaProvider { private ILocalAuthenticationResult mLocalAuthenticationResult; private TokenResult mTokenResult; @@ -49,6 +51,9 @@ public class AcquireTokenResult implements IBrokerPerformanceMetricsProvider, IB private BrokerPerformanceMetrics mBrokerPerformanceMetrics; + @Nullable + private TelemetrySchema mTelemetrySchema; + public void setLocalAuthenticationResult(ILocalAuthenticationResult result) { this.mLocalAuthenticationResult = result; this.mSucceeded = true; @@ -75,6 +80,17 @@ public BrokerPerformanceMetrics getBrokerPerformanceMetrics() { return this.mBrokerPerformanceMetrics; } + @Override + public void setTelemetrySchema(@Nullable final TelemetrySchema telemetrySchema) { + this.mTelemetrySchema = telemetrySchema; + } + + @Override + @Nullable + public TelemetrySchema getTelemetrySchema() { + return this.mTelemetrySchema; + } + // Suppressing rawtype warnings due to the generic type AuthorizationResult @SuppressWarnings(WarningType.rawtype_warning) public AuthorizationResult getAuthorizationResult() { diff --git a/common4j/src/test/com/microsoft/identity/common/java/broker/telemetry/EventCollectorTest.kt b/common4j/src/test/com/microsoft/identity/common/java/broker/telemetry/EventCollectorTest.kt new file mode 100644 index 0000000000..e2e4c5eb93 --- /dev/null +++ b/common4j/src/test/com/microsoft/identity/common/java/broker/telemetry/EventCollectorTest.kt @@ -0,0 +1,165 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.common.java.broker.telemetry + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * Unit tests for [EventCollector]. + */ +class EventCollectorTest { + + private val testCorrelationId = "test-correlation-id" + + @Test + fun addEvent_whenTagProvided_eventIsPresentInSchema() { + val collector = EventCollector(testCorrelationId) + + collector.addEvent(EventTag.BrokerRequestReceived) + + val schema = collector.toTelemetrySchema() + val events = schema.performanceRecord?.executionFlow + assertNotNull(events) + assertEquals(1, events!!.size) + assertEquals(EventTag.BrokerRequestReceived, events[0].tag) + } + + @Test + fun addEvent_withDiagnosticAndErrorCode_fieldsArePresentInEvent() { + val collector = EventCollector(testCorrelationId) + + collector.addEvent(EventTag.BrokerRequestFailed, diagnosticCode = 42, errorCode = 100) + + val events = collector.toTelemetrySchema().performanceRecord?.executionFlow + assertNotNull(events) + val event = events!![0] + assertEquals(42, event.diagnosticCode) + assertEquals(100, event.errorCode) + } + + @Test + fun addEvent_whenNoOptionalCodes_diagnosticAndErrorCodeAreNull() { + val collector = EventCollector(testCorrelationId) + + collector.addEvent(EventTag.BrokerCacheHit) + + val event = collector.toTelemetrySchema().performanceRecord?.executionFlow?.get(0) + assertNotNull(event) + assertNull(event!!.diagnosticCode) + assertNull(event.errorCode) + } + + @Test + fun addEvent_multipleEvents_allEventsRetainedInOrder() { + val collector = EventCollector(testCorrelationId) + val tags = listOf( + EventTag.BrokerRequestReceived, + EventTag.BrokerCacheCheckStart, + EventTag.BrokerCacheHit, + EventTag.BrokerResponseSent + ) + + tags.forEach { collector.addEvent(it) } + + val events = collector.toTelemetrySchema().performanceRecord?.executionFlow + assertNotNull(events) + assertEquals(tags.size, events!!.size) + tags.forEachIndexed { index, tag -> assertEquals(tag, events[index].tag) } + } + + @Test + fun toTelemetrySchema_correlationIdMatchesConstructorArg() { + val collector = EventCollector(testCorrelationId) + + val schema = collector.toTelemetrySchema() + + assertEquals(testCorrelationId, schema.correlationId) + } + + @Test + fun toTelemetrySchema_performanceRecordHasNonNegativeDuration() { + val collector = EventCollector(testCorrelationId) + collector.addEvent(EventTag.BrokerRequestReceived) + + val schema = collector.toTelemetrySchema() + + assertNotNull(schema.performanceRecord) + assertTrue(schema.performanceRecord!!.duration >= 0) + } + + @Test + fun toTelemetrySchema_startTimeIsIso8601Format() { + val collector = EventCollector(testCorrelationId) + + val schema = collector.toTelemetrySchema() + + val startTime = schema.performanceRecord?.startTime + assertNotNull(startTime) + // ISO 8601 instant strings end with 'Z' + assertTrue(startTime!!.endsWith("Z")) + } + + @Test + fun addEvent_isThreadSafe_allEventsRecordedUnderConcurrentAccess() { + val collector = EventCollector(testCorrelationId) + val threadCount = 10 + val eventsPerThread = 20 + val threads = (1..threadCount).map { + Thread { + repeat(eventsPerThread) { + collector.addEvent(EventTag.BrokerNetworkCallStart) + } + } + } + + threads.forEach { it.start() } + threads.forEach { it.join() } + + val events = collector.toTelemetrySchema().performanceRecord?.executionFlow + assertNotNull(events) + assertEquals(threadCount * eventsPerThread, events!!.size) + } + + @Test + fun addEventSafely_withNullCollector_doesNotThrow() { + // Should be a no-op and not throw NullPointerException + TelemetryHelper.addEventSafely(null, EventTag.BrokerRequestReceived) + } + + @Test + fun addEventSafely_withNonNullCollector_addsEvent() { + val collector = EventCollector(testCorrelationId) + + TelemetryHelper.addEventSafely(collector, EventTag.BrokerTokenAcquired, diagnosticCode = 1) + + val events = collector.toTelemetrySchema().performanceRecord?.executionFlow + assertNotNull(events) + assertEquals(1, events!!.size) + assertEquals(EventTag.BrokerTokenAcquired, events[0].tag) + assertEquals(1, events[0].diagnosticCode) + } +} diff --git a/common4j/src/test/com/microsoft/identity/common/java/broker/telemetry/TelemetrySchemaSerializationTest.kt b/common4j/src/test/com/microsoft/identity/common/java/broker/telemetry/TelemetrySchemaSerializationTest.kt new file mode 100644 index 0000000000..f0ca12383a --- /dev/null +++ b/common4j/src/test/com/microsoft/identity/common/java/broker/telemetry/TelemetrySchemaSerializationTest.kt @@ -0,0 +1,237 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.common.java.broker.telemetry + +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test + +/** + * Unit tests for Gson round-trip serialization/deserialization of [TelemetrySchema] and + * its nested types. + */ +class TelemetrySchemaSerializationTest { + + private val gson: Gson = GsonBuilder().create() + + // ------------------------------------------------------------------ + // TelemetrySchema round-trip + // ------------------------------------------------------------------ + + @Test + fun telemetrySchema_roundTrip_preservesCorrelationId() { + val original = buildFullSchema() + + val json = gson.toJson(original) + val restored = gson.fromJson(json, TelemetrySchema::class.java) + + assertEquals(original.correlationId, restored.correlationId) + } + + @Test + fun telemetrySchema_roundTrip_preservesSchemaVersion() { + val original = buildFullSchema() + + val json = gson.toJson(original) + val restored = gson.fromJson(json, TelemetrySchema::class.java) + + assertEquals(TelemetrySchema.CURRENT_VERSION, restored.schemaVersion) + } + + @Test + fun telemetrySchema_roundTrip_preservesOptionalFields() { + val original = buildFullSchema() + + val json = gson.toJson(original) + val restored = gson.fromJson(json, TelemetrySchema::class.java) + + assertEquals(original.authOutcome, restored.authOutcome) + assertEquals(original.errorCode, restored.errorCode) + assertEquals(original.responseStarvationDuration, restored.responseStarvationDuration) + assertEquals(original.powerPolicy, restored.powerPolicy) + assertEquals(original.deviceIdle, restored.deviceIdle) + } + + @Test + fun telemetrySchema_roundTrip_withNullOptionalFields_remainsNull() { + val original = TelemetrySchema(correlationId = "corr-null-opts") + + val json = gson.toJson(original) + val restored = gson.fromJson(json, TelemetrySchema::class.java) + + assertNull(restored.authOutcome) + assertNull(restored.errorCode) + assertNull(restored.performanceRecord) + } + + // ------------------------------------------------------------------ + // PerformanceRecord round-trip + // ------------------------------------------------------------------ + + @Test + fun performanceRecord_roundTrip_preservesDuration() { + val original = buildFullSchema() + + val json = gson.toJson(original) + val restored = gson.fromJson(json, TelemetrySchema::class.java) + + assertEquals(original.performanceRecord!!.duration, restored.performanceRecord!!.duration) + } + + @Test + fun performanceRecord_roundTrip_preservesStartTime() { + val original = buildFullSchema() + + val json = gson.toJson(original) + val restored = gson.fromJson(json, TelemetrySchema::class.java) + + assertEquals(original.performanceRecord!!.startTime, restored.performanceRecord!!.startTime) + } + + // ------------------------------------------------------------------ + // ExecutionEvent round-trip + // ------------------------------------------------------------------ + + @Test + fun executionEvent_roundTrip_preservesTag() { + val original = buildFullSchema() + + val json = gson.toJson(original) + val restored = gson.fromJson(json, TelemetrySchema::class.java) + + val originalEvents = original.performanceRecord!!.executionFlow + val restoredEvents = restored.performanceRecord!!.executionFlow + assertEquals(originalEvents.size, restoredEvents.size) + originalEvents.forEachIndexed { i, event -> + assertEquals(event.tag, restoredEvents[i].tag) + } + } + + @Test + fun executionEvent_roundTrip_preservesTimestamp() { + val event = ExecutionEvent(tag = EventTag.BrokerCacheHit, timestampMs = 12345L) + + val json = gson.toJson(event) + val restored = gson.fromJson(json, ExecutionEvent::class.java) + + assertEquals(12345L, restored.timestampMs) + } + + @Test + fun executionEvent_roundTrip_preservesOptionalDiagnosticAndErrorCode() { + val event = ExecutionEvent( + tag = EventTag.BrokerNetworkCallFailed, + timestampMs = 5000L, + diagnosticCode = 7, + errorCode = 503 + ) + + val json = gson.toJson(event) + val restored = gson.fromJson(json, ExecutionEvent::class.java) + + assertEquals(7, restored.diagnosticCode) + assertEquals(503, restored.errorCode) + } + + @Test + fun executionEvent_roundTrip_whenOptionalCodesAbsent_remainsNull() { + val event = ExecutionEvent(tag = EventTag.BrokerRequestReceived, timestampMs = 0L) + + val json = gson.toJson(event) + val restored = gson.fromJson(json, ExecutionEvent::class.java) + + assertNull(restored.diagnosticCode) + assertNull(restored.errorCode) + } + + // ------------------------------------------------------------------ + // EventTag serialized name verification + // ------------------------------------------------------------------ + + @Test + fun eventTag_serializedNameIsCompactValue() { + val event = ExecutionEvent(tag = EventTag.BrokerRequestReceived, timestampMs = 0L) + + val json = gson.toJson(event) + + // The tag should be serialized as its compact value, not the enum name + assertNotNull(json) + // The 't' field in JSON should contain the compact value string + val tagJson = gson.toJsonTree(event).asJsonObject.get("t") + assertNotNull(tagJson) + // Tag is serialized as an object with "v" field containing the compact value + val tagValue = tagJson.asJsonObject.get("v").asString + assertEquals("bre.recv", tagValue) + } + + // ------------------------------------------------------------------ + // EventCollector → TelemetrySchema integration + // ------------------------------------------------------------------ + + @Test + fun eventCollector_toTelemetrySchema_roundTripsCorrectly() { + val collector = EventCollector("round-trip-corr-id") + collector.addEvent(EventTag.BrokerRequestReceived) + collector.addEvent(EventTag.BrokerCacheCheckStart) + collector.addEvent(EventTag.BrokerResponseSent) + + val original = collector.toTelemetrySchema() + val json = gson.toJson(original) + val restored = gson.fromJson(json, TelemetrySchema::class.java) + + assertEquals(original.correlationId, restored.correlationId) + assertEquals(3, restored.performanceRecord?.executionFlow?.size) + assertEquals(EventTag.BrokerResponseSent, restored.performanceRecord?.executionFlow?.last()?.tag) + } + + // ------------------------------------------------------------------ + // Helpers + // ------------------------------------------------------------------ + + private fun buildFullSchema(): TelemetrySchema { + val events = listOf( + ExecutionEvent(tag = EventTag.BrokerRequestReceived, timestampMs = 0L), + ExecutionEvent(tag = EventTag.BrokerCacheHit, timestampMs = 10L), + ExecutionEvent(tag = EventTag.BrokerResponseSent, timestampMs = 50L) + ) + val perfRecord = PerformanceRecord( + startTime = "2024-01-01T00:00:00Z", + duration = 50L, + executionFlow = events + ) + return TelemetrySchema( + correlationId = "test-corr-id", + name = "AcquireTokenSilent", + version = "1.0", + authOutcome = "success", + errorCode = null, + responseStarvationDuration = 5, + powerPolicy = false, + deviceIdle = true, + performanceRecord = perfRecord + ) + } +} From 2f06a931904d9172f95a11d49e895e35f66a14be Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 21:46:50 +0000 Subject: [PATCH 3/4] Address code review feedback: clarify test helper SDK version value Agent-Logs-Url: https://github.com/AzureAD/microsoft-authentication-library-common-for-android/sessions/09541e92-6c54-4fd8-beab-81cca1bfae45 Co-authored-by: Prvnkmr337 <198076477+Prvnkmr337@users.noreply.github.com> --- .../java/broker/telemetry/TelemetrySchemaSerializationTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common4j/src/test/com/microsoft/identity/common/java/broker/telemetry/TelemetrySchemaSerializationTest.kt b/common4j/src/test/com/microsoft/identity/common/java/broker/telemetry/TelemetrySchemaSerializationTest.kt index f0ca12383a..f601089f19 100644 --- a/common4j/src/test/com/microsoft/identity/common/java/broker/telemetry/TelemetrySchemaSerializationTest.kt +++ b/common4j/src/test/com/microsoft/identity/common/java/broker/telemetry/TelemetrySchemaSerializationTest.kt @@ -225,7 +225,7 @@ class TelemetrySchemaSerializationTest { return TelemetrySchema( correlationId = "test-corr-id", name = "AcquireTokenSilent", - version = "1.0", + version = "test-sdk-1.0", authOutcome = "success", errorCode = null, responseStarvationDuration = 5, From 46e67ff017bab586183ba6b499fdd7cbe75bce7d Mon Sep 17 00:00:00 2001 From: prsaminathan Date: Thu, 23 Apr 2026 14:53:41 -0700 Subject: [PATCH 4/4] Address PR review feedback for TelemetrySchema data model (AB#3584247) - Remove compact value property from EventTag enum (dead code, design uses human-readable names) - Apply BROKER_THREAD_ID_OFFSET (10,000) in EventCollector to avoid collision with OneAuth client-side thread IDs - Fix race condition in EventCollector.toTelemetrySchema(): snapshot events before capturing duration - Give PerformanceRecord its own CURRENT_PERF_VERSION constant (independent of TelemetrySchema version) - Add thread-safety documentation to ITelemetrySchemaProvider (set-before-share contract) - Fix TelemetrySchemaSerializationTest: assert enum name instead of removed compact value - Add duration invariant assertion to EventCollectorTest concurrency test - Fix JUnit 4 assertTrue argument order in EventCollectorTest --- .../java/broker/telemetry/EventCollector.kt | 20 ++++++- .../common/java/broker/telemetry/EventTag.kt | 55 +++++++++---------- .../telemetry/ITelemetrySchemaProvider.kt | 4 ++ .../broker/telemetry/PerformanceRecord.kt | 15 ++++- .../broker/telemetry/EventCollectorTest.kt | 11 +++- .../TelemetrySchemaSerializationTest.kt | 10 ++-- 6 files changed, 75 insertions(+), 40 deletions(-) diff --git a/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/EventCollector.kt b/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/EventCollector.kt index 63d2072005..b3d5b87c12 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/EventCollector.kt +++ b/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/EventCollector.kt @@ -37,8 +37,19 @@ class EventCollector(private val correlationId: String) { private val events = CopyOnWriteArrayList() private val startTimeMs: Long = System.currentTimeMillis() + companion object { + /** + * Offset applied to broker-side thread IDs to avoid collision with + * OneAuth client-side thread IDs when the two event streams are + * stitched in Kusto via correlation_id. + */ + const val BROKER_THREAD_ID_OFFSET = 10_000L + } + /** * Records a new [ExecutionEvent] with the current elapsed time. + * Thread ID is offset by [BROKER_THREAD_ID_OFFSET] to avoid collision + * with client-side (OneAuth) thread IDs during Kusto stitching. * * @param tag The [EventTag] identifying this phase. * @param diagnosticCode Optional diagnostic code for additional context. @@ -49,6 +60,7 @@ class EventCollector(private val correlationId: String) { ExecutionEvent( tag = tag, timestampMs = System.currentTimeMillis() - startTimeMs, + threadId = Thread.currentThread().id + BROKER_THREAD_ID_OFFSET, diagnosticCode = diagnosticCode, errorCode = errorCode ) @@ -57,18 +69,22 @@ class EventCollector(private val correlationId: String) { /** * Builds a [TelemetrySchema] from all events collected so far. - * The returned schema captures the total elapsed duration and the full event timeline. + * + * Snapshots the event list first, then captures the end time, ensuring + * that [PerformanceRecord.duration] is always >= the last event's timestamp. * * @return A [TelemetrySchema] containing a [PerformanceRecord] with all collected events. */ fun toTelemetrySchema(): TelemetrySchema { + // Snapshot events first, then capture end time — guarantees duration >= last event ts. + val eventSnapshot = events.toList() val duration = System.currentTimeMillis() - startTimeMs return TelemetrySchema( correlationId = correlationId, performanceRecord = PerformanceRecord( startTime = Instant.ofEpochMilli(startTimeMs).toString(), duration = duration, - executionFlow = events.toList() + executionFlow = eventSnapshot ) ) } diff --git a/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/EventTag.kt b/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/EventTag.kt index a8219e734c..761837819e 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/EventTag.kt +++ b/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/EventTag.kt @@ -22,41 +22,40 @@ // THE SOFTWARE. package com.microsoft.identity.common.java.broker.telemetry -import com.google.gson.annotations.SerializedName - /** * Enum of event tags representing phases of the broker authentication flow. - * Each tag has a compact serialized value to minimize payload size. + * Serialized as human-readable enum names (e.g., "BrokerCacheHit") on the IPC wire format. + * Compact JSON keys on [ExecutionEvent] fields (t, ts, tid, d, e) minimize payload size. */ -enum class EventTag(@SerializedName("v") val value: String) { +enum class EventTag { // BrokerEntry (5) - BrokerRequestReceived("bre.recv"), - BrokerRequestDeserialized("bre.dser"), - BrokerAccountLookupStart("bre.als"), - BrokerAccountLookupEnd("bre.ale"), - BrokerRequestValidated("bre.val"), + BrokerRequestReceived, + BrokerRequestDeserialized, + BrokerAccountLookupStart, + BrokerAccountLookupEnd, + BrokerRequestValidated, // BrokerDispatch (3) - BrokerControllerSelected("bdi.csel"), - BrokerCommandQueued("bdi.cq"), - BrokerCommandExecutionStart("bdi.cex"), + BrokerControllerSelected, + BrokerCommandQueued, + BrokerCommandExecutionStart, // BrokerCache (6) - BrokerCacheCheckStart("bca.cks"), - BrokerCacheCheckEnd("bca.cke"), - BrokerCacheHit("bca.hit"), - BrokerCacheMiss("bca.miss"), - BrokerCacheWriteStart("bca.wrs"), - BrokerCacheWriteEnd("bca.wre"), + BrokerCacheCheckStart, + BrokerCacheCheckEnd, + BrokerCacheHit, + BrokerCacheMiss, + BrokerCacheWriteStart, + BrokerCacheWriteEnd, // BrokerNetwork (5) - BrokerPrtLoadStart("bne.prt"), - BrokerNetworkCallStart("bne.ncs"), - BrokerNetworkCallEnd("bne.nce"), - BrokerTokenAcquired("bne.tok"), - BrokerNetworkCallFailed("bne.nfl"), + BrokerPrtLoadStart, + BrokerNetworkCallStart, + BrokerNetworkCallEnd, + BrokerTokenAcquired, + BrokerNetworkCallFailed, // BrokerResponse (3) - BrokerResponseSerialized("brs.ser"), - BrokerResponseSent("brs.snt"), - BrokerRequestFailed("brs.fail"), + BrokerResponseSerialized, + BrokerResponseSent, + BrokerRequestFailed, // CommonStrategy (2) - CommonHttpRequestExecute("cst.hreq"), - CommonHttpResponseReceived("cst.hrsp") + CommonHttpRequestExecute, + CommonHttpResponseReceived } diff --git a/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/ITelemetrySchemaProvider.kt b/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/ITelemetrySchemaProvider.kt index 6815278cab..fbfe65fa30 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/ITelemetrySchemaProvider.kt +++ b/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/ITelemetrySchemaProvider.kt @@ -25,6 +25,10 @@ package com.microsoft.identity.common.java.broker.telemetry /** * Implemented by objects that can carry a [TelemetrySchema]. * Mainly used to pass the structured broker telemetry between broker and client. + * + * Thread-safety note: [telemetrySchema] must be set before the implementing object + * is shared across threads. The IPC boundary (Binder transaction) provides the + * memory barrier. This follows the same contract as [IBrokerPerformanceMetricsProvider]. */ interface ITelemetrySchemaProvider { var telemetrySchema: TelemetrySchema? diff --git a/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/PerformanceRecord.kt b/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/PerformanceRecord.kt index e683ebe2ed..64a0c75fdf 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/PerformanceRecord.kt +++ b/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/PerformanceRecord.kt @@ -27,14 +27,23 @@ import com.google.gson.annotations.SerializedName /** * Aggregation container for the performance data captured during an authentication flow. * - * @param version Schema version for this record. Defaults to [TelemetrySchema.CURRENT_VERSION]. + * @param version Schema version for this perf section. Independent of [TelemetrySchema.CURRENT_VERSION]. * @param startTime ISO 8601 UTC timestamp when the flow started. * @param duration Total elapsed time in milliseconds for the authentication flow. * @param executionFlow Ordered list of [ExecutionEvent]s captured during the flow. */ data class PerformanceRecord( - @SerializedName("version") val version: String = TelemetrySchema.CURRENT_VERSION, + @SerializedName("version") val version: String = CURRENT_PERF_VERSION, @SerializedName("start_time") val startTime: String, @SerializedName("duration") val duration: Long, @SerializedName("execution_flow") val executionFlow: List -) +) { + companion object { + /** + * Version of the perf section schema. Informational only — not used in + * version negotiation (which uses root-level [TelemetrySchema.CURRENT_VERSION]). + * Evolves independently from the top-level schema version. + */ + const val CURRENT_PERF_VERSION = "1.0.0" + } +} diff --git a/common4j/src/test/com/microsoft/identity/common/java/broker/telemetry/EventCollectorTest.kt b/common4j/src/test/com/microsoft/identity/common/java/broker/telemetry/EventCollectorTest.kt index e2e4c5eb93..16bade96bc 100644 --- a/common4j/src/test/com/microsoft/identity/common/java/broker/telemetry/EventCollectorTest.kt +++ b/common4j/src/test/com/microsoft/identity/common/java/broker/telemetry/EventCollectorTest.kt @@ -139,9 +139,18 @@ class EventCollectorTest { threads.forEach { it.start() } threads.forEach { it.join() } - val events = collector.toTelemetrySchema().performanceRecord?.executionFlow + val schema = collector.toTelemetrySchema() + val events = schema.performanceRecord?.executionFlow assertNotNull(events) assertEquals(threadCount * eventsPerThread, events!!.size) + + // Verify duration invariant: duration >= max event timestamp (race condition regression test) + val maxEventTs = events.maxOf { it.timestampMs } + val duration = schema.performanceRecord!!.duration + assertTrue( + "Duration ($duration) must be >= max event timestamp ($maxEventTs)", + duration >= maxEventTs + ) } @Test diff --git a/common4j/src/test/com/microsoft/identity/common/java/broker/telemetry/TelemetrySchemaSerializationTest.kt b/common4j/src/test/com/microsoft/identity/common/java/broker/telemetry/TelemetrySchemaSerializationTest.kt index f601089f19..879617b7d0 100644 --- a/common4j/src/test/com/microsoft/identity/common/java/broker/telemetry/TelemetrySchemaSerializationTest.kt +++ b/common4j/src/test/com/microsoft/identity/common/java/broker/telemetry/TelemetrySchemaSerializationTest.kt @@ -172,19 +172,17 @@ class TelemetrySchemaSerializationTest { // ------------------------------------------------------------------ @Test - fun eventTag_serializedNameIsCompactValue() { + fun eventTag_serializedAsEnumName() { val event = ExecutionEvent(tag = EventTag.BrokerRequestReceived, timestampMs = 0L) val json = gson.toJson(event) - // The tag should be serialized as its compact value, not the enum name + // The tag should be serialized as the human-readable enum name assertNotNull(json) - // The 't' field in JSON should contain the compact value string val tagJson = gson.toJsonTree(event).asJsonObject.get("t") assertNotNull(tagJson) - // Tag is serialized as an object with "v" field containing the compact value - val tagValue = tagJson.asJsonObject.get("v").asString - assertEquals("bre.recv", tagValue) + // EventTag is a plain enum — Gson serializes it as its .name() + assertEquals("BrokerRequestReceived", tagJson.asString) } // ------------------------------------------------------------------