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..ba6bc5dc7d 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/AuthenticationConstants.java +++ b/common4j/src/main/com/microsoft/identity/common/java/AuthenticationConstants.java @@ -624,6 +624,16 @@ public static final class Broker { * Value for nativebroker mode sent in the extra query param by ESTS when the request is from lookup. */ public static final String LOOKUP_MODE_VALUE = "Lookup"; + + /** + * Bundle key for the broker telemetry request payload. + */ + public static final String BROKER_TELEMETRY_REQUEST = "broker_telemetry_request"; + + /** + * Bundle key for the broker telemetry schema payload. + */ + public static final String BROKER_TELEMETRY_SCHEMA = "broker_telemetry_schema"; } @NoArgsConstructor(access = AccessLevel.PRIVATE) 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..c3bdc00e80 --- /dev/null +++ b/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/EventCollector.kt @@ -0,0 +1,76 @@ +// 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 of [ExecutionEvent] instances for a single authentication request. + * + * Call [addEvent] from any thread to record a phase transition. Call [toTelemetrySchema] when + * the request is complete to produce a [TelemetrySchema] with the full event timeline. + * + * @param correlationId Correlation ID for 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 elapsed time since collection started. + * The resulting [ExecutionEvent.timestampMs] is relative elapsed milliseconds from + * collector start — not an absolute epoch timestamp. + * + * @param tag The [EventTag] identifying the phase. + * @param diagnosticCode Optional diagnostic code for additional context. + * @param errorCode Optional error code if this event marks a failure condition. + */ + 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. + * + * @return A [TelemetrySchema] containing a [PerformanceRecord] with the full event timeline. + */ + 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..e04fad4654 --- /dev/null +++ b/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/EventTag.kt @@ -0,0 +1,66 @@ +// 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 distinct phases in the broker authentication flow. + * Each tag has a compact serialized value for efficient IPC transport. + * + * @param value Compact serialized form used in JSON (e.g., "bre.recv" for BrokerRequestReceived). + * Prefix convention: bre=BrokerEntry, bdi=BrokerDispatch, bca=BrokerCache, + * bne=BrokerNetwork, brs=BrokerResponse, cst=CommonStrategy. + */ +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..2b419dee34 --- /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 captured during a broker authentication flow. + * + * @param tag The [EventTag] identifying the phase of the authentication flow. + * @param timestampMs Elapsed milliseconds since the start of the [EventCollector]. + * @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 this event marks a failure condition. + */ +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..a7dabbae3c --- /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 classes that carry a [TelemetrySchema] instance, enabling structured + * per-request performance data to flow from the broker through the authentication result chain. + */ +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..31d8940408 --- /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 + +/** + * Container aggregating timing data for a single authentication request. + * + * @param version Schema version of this record (defaults to "1.0.0"). + * @param startTime ISO 8601 UTC timestamp when the request was initiated. + * @param duration Total elapsed time in milliseconds from start to completion. + * @param executionFlow Ordered list of [ExecutionEvent] entries captured during the flow. + */ +data class PerformanceRecord( + @SerializedName("version") val version: String = "1.0.0", + @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..4170b9db20 --- /dev/null +++ b/common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/TelemetryHelper.kt @@ -0,0 +1,48 @@ +// 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 telemetry events via an optional [EventCollector]. + * + * All methods are no-ops when [collector] is null, so callers do not need to perform null checks. + */ +object TelemetryHelper { + /** + * Safely adds an event to [collector] if it is non-null. + * + * @param collector The [EventCollector] to record into, or null to skip recording. + * @param tag The [EventTag] identifying the phase. + * @param diagnosticCode Optional diagnostic code for additional context. + * @param errorCode Optional error code if this event marks a failure condition. + */ + @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..dc15fc13d7 --- /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 each authentication request. + * Carries structured timing, outcome, and policy metadata that client SDKs can surface + * as per-request performance breakdowns. + * + * @param schemaVersion Version of this schema (defaults to [CURRENT_VERSION]). + * @param correlationId Correlation ID linking this schema to the authentication request. + * @param name Optional descriptive name for the operation. + * @param version Optional version of the operation. + * @param authOutcome Optional outcome of the authentication flow (e.g., "success", "failure"). + * @param errorCode Optional error code when authentication fails. + * @param responseStarvationDuration Optional duration (ms) the response was starved before processing. + * @param powerPolicy Optional flag indicating whether a power policy was active. + * @param deviceIdle Optional flag indicating whether the device was idle during the flow. + * @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..19aa67bddd 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,16 @@ public BrokerPerformanceMetrics getBrokerPerformanceMetrics() { return this.mBrokerPerformanceMetrics; } + 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..257d82db8b 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,16 @@ public BrokerPerformanceMetrics getBrokerPerformanceMetrics() { return this.mBrokerPerformanceMetrics; } + 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..7ee9170738 --- /dev/null +++ b/common4j/src/test/com/microsoft/identity/common/java/broker/telemetry/EventCollectorTest.kt @@ -0,0 +1,170 @@ +// 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 +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors + +/** + * Unit tests for [EventCollector]. + */ +class EventCollectorTest { + + private val correlationId = "test-correlation-id" + + @Test + fun addEvent_whenCalled_addsEventToFlow() { + val collector = EventCollector(correlationId) + + collector.addEvent(EventTag.BrokerRequestReceived) + + val schema = collector.toTelemetrySchema() + assertEquals(1, schema.performanceRecord?.executionFlow?.size) + assertEquals(EventTag.BrokerRequestReceived, schema.performanceRecord?.executionFlow?.first()?.tag) + } + + @Test + fun addEvent_withDiagnosticCode_setsCodeOnEvent() { + val collector = EventCollector(correlationId) + + collector.addEvent(EventTag.BrokerCacheMiss, diagnosticCode = 42) + + val event = collector.toTelemetrySchema().performanceRecord?.executionFlow?.first() + assertEquals(42, event?.diagnosticCode) + assertNull(event?.errorCode) + } + + @Test + fun addEvent_withErrorCode_setsErrorCodeOnEvent() { + val collector = EventCollector(correlationId) + + collector.addEvent(EventTag.BrokerRequestFailed, errorCode = 999) + + val event = collector.toTelemetrySchema().performanceRecord?.executionFlow?.first() + assertEquals(999, event?.errorCode) + assertNull(event?.diagnosticCode) + } + + @Test + fun toTelemetrySchema_whenNoEvents_returnsEmptyExecutionFlow() { + val collector = EventCollector(correlationId) + + val schema = collector.toTelemetrySchema() + + assertNotNull(schema.performanceRecord) + assertTrue(schema.performanceRecord!!.executionFlow.isEmpty()) + } + + @Test + fun toTelemetrySchema_setsCorrelationId() { + val collector = EventCollector(correlationId) + + val schema = collector.toTelemetrySchema() + + assertEquals(correlationId, schema.correlationId) + } + + @Test + fun toTelemetrySchema_setsNonNegativeDuration() { + val collector = EventCollector(correlationId) + collector.addEvent(EventTag.BrokerRequestReceived) + + val schema = collector.toTelemetrySchema() + + assertTrue(schema.performanceRecord!!.duration >= 0) + } + + @Test + fun toTelemetrySchema_setsStartTimeAsIso8601() { + val collector = EventCollector(correlationId) + + val schema = collector.toTelemetrySchema() + + // Instant.toString() produces a valid ISO-8601 string containing 'T' + val startTime = schema.performanceRecord?.startTime + assertNotNull(startTime) + assertTrue(startTime!!.contains("T")) + } + + @Test + fun addEvent_multipleEvents_preservesInsertionOrder() { + val collector = EventCollector(correlationId) + val tags = listOf( + EventTag.BrokerRequestReceived, + EventTag.BrokerCacheCheckStart, + EventTag.BrokerCacheHit, + EventTag.BrokerResponseSent + ) + + tags.forEach { collector.addEvent(it) } + + val flow = collector.toTelemetrySchema().performanceRecord!!.executionFlow + assertEquals(tags.size, flow.size) + tags.forEachIndexed { index, tag -> assertEquals(tag, flow[index].tag) } + } + + @Test + fun addEvent_fromMultipleThreads_isThreadSafe() { + val collector = EventCollector(correlationId) + val threadCount = 50 + val latch = CountDownLatch(threadCount) + val executor = Executors.newFixedThreadPool(threadCount) + + repeat(threadCount) { + executor.submit { + collector.addEvent(EventTag.BrokerNetworkCallStart) + latch.countDown() + } + } + + latch.await() + executor.shutdown() + + val flow = collector.toTelemetrySchema().performanceRecord!!.executionFlow + assertEquals(threadCount, flow.size) + } + + @Test + fun addEventSafely_whenCollectorIsNull_doesNotThrow() { + // Should be a no-op and not throw NullPointerException + TelemetryHelper.addEventSafely(null, EventTag.BrokerRequestReceived) + } + + @Test + fun addEventSafely_whenCollectorIsNonNull_addsEvent() { + val collector = EventCollector(correlationId) + + TelemetryHelper.addEventSafely(collector, EventTag.BrokerCacheHit, diagnosticCode = 1, errorCode = 2) + + val flow = collector.toTelemetrySchema().performanceRecord!!.executionFlow + assertEquals(1, flow.size) + assertEquals(EventTag.BrokerCacheHit, flow.first().tag) + assertEquals(1, flow.first().diagnosticCode) + assertEquals(2, flow.first().errorCode) + } +} 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..8afbe783ec --- /dev/null +++ b/common4j/src/test/com/microsoft/identity/common/java/broker/telemetry/TelemetrySchemaSerializationTest.kt @@ -0,0 +1,183 @@ +// 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 verifying that [TelemetrySchema] and its nested types round-trip through + * Gson serialization/deserialization correctly, including [EventTag] compact values. + */ +class TelemetrySchemaSerializationTest { + + private val gson: Gson = GsonBuilder().create() + + @Test + fun telemetrySchema_roundTrip_preservesCorrelationId() { + val original = TelemetrySchema(correlationId = "abc-123") + + val json = gson.toJson(original) + val restored = gson.fromJson(json, TelemetrySchema::class.java) + + assertEquals("abc-123", restored.correlationId) + } + + @Test + fun telemetrySchema_roundTrip_preservesSchemaVersion() { + val original = TelemetrySchema(correlationId = "abc-123") + + 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 = TelemetrySchema( + correlationId = "cid", + name = "token.acquire", + version = "2", + authOutcome = "success", + errorCode = null, + responseStarvationDuration = 100, + powerPolicy = true, + deviceIdle = false + ) + + val json = gson.toJson(original) + val restored = gson.fromJson(json, TelemetrySchema::class.java) + + assertEquals("token.acquire", restored.name) + assertEquals("2", restored.version) + assertEquals("success", restored.authOutcome) + assertNull(restored.errorCode) + assertEquals(100, restored.responseStarvationDuration) + assertEquals(true, restored.powerPolicy) + assertEquals(false, restored.deviceIdle) + } + + @Test + fun performanceRecord_roundTrip_preservesFields() { + val events = listOf( + ExecutionEvent(tag = EventTag.BrokerRequestReceived, timestampMs = 0L), + ExecutionEvent(tag = EventTag.BrokerCacheHit, timestampMs = 10L) + ) + val perf = PerformanceRecord( + startTime = "2024-01-01T00:00:00Z", + duration = 150L, + executionFlow = events + ) + val schema = TelemetrySchema(correlationId = "cid", performanceRecord = perf) + + val json = gson.toJson(schema) + val restored = gson.fromJson(json, TelemetrySchema::class.java) + + val restoredPerf = restored.performanceRecord + assertNotNull(restoredPerf) + assertEquals("1.0.0", restoredPerf!!.version) + assertEquals("2024-01-01T00:00:00Z", restoredPerf.startTime) + assertEquals(150L, restoredPerf.duration) + assertEquals(2, restoredPerf.executionFlow.size) + } + + @Test + fun eventTag_serialized_usesCompactValue() { + val event = ExecutionEvent(tag = EventTag.BrokerRequestReceived, timestampMs = 0L) + val json = gson.toJson(event) + // The tag field should serialize using compact "v" value, not the enum name + assert(json.contains("bre.recv")) { "Expected compact tag value 'bre.recv' in: $json" } + } + + @Test + fun executionEvent_roundTrip_preservesTagEnum() { + val original = ExecutionEvent( + tag = EventTag.BrokerNetworkCallFailed, + timestampMs = 42L, + diagnosticCode = 5, + errorCode = 99 + ) + + val json = gson.toJson(original) + val restored = gson.fromJson(json, ExecutionEvent::class.java) + + assertEquals(EventTag.BrokerNetworkCallFailed, restored.tag) + assertEquals(42L, restored.timestampMs) + assertEquals(5, restored.diagnosticCode) + assertEquals(99, restored.errorCode) + } + + @Test + fun executionEvent_roundTrip_withNullOptionalFields() { + val original = ExecutionEvent(tag = EventTag.BrokerCacheCheckStart, timestampMs = 7L) + + val json = gson.toJson(original) + val restored = gson.fromJson(json, ExecutionEvent::class.java) + + assertNull(restored.diagnosticCode) + assertNull(restored.errorCode) + } + + @Test + fun eventTag_allValues_roundTripThroughGson() { + for (tag in EventTag.values()) { + val event = ExecutionEvent(tag = tag, timestampMs = 0L) + val json = gson.toJson(event) + val restored = gson.fromJson(json, ExecutionEvent::class.java) + assertEquals("Round-trip failed for tag: $tag", tag, restored.tag) + } + } + + @Test + fun eventCollector_toTelemetrySchema_roundTripsThroughGson() { + val collector = EventCollector("test-cid") + collector.addEvent(EventTag.BrokerRequestReceived) + collector.addEvent(EventTag.BrokerCacheCheckStart) + collector.addEvent(EventTag.BrokerResponseSent) + + val schema = collector.toTelemetrySchema() + val json = gson.toJson(schema) + val restored = gson.fromJson(json, TelemetrySchema::class.java) + + assertEquals("test-cid", restored.correlationId) + assertEquals(3, restored.performanceRecord?.executionFlow?.size) + assertEquals(EventTag.BrokerRequestReceived, restored.performanceRecord?.executionFlow?.get(0)?.tag) + assertEquals(EventTag.BrokerCacheCheckStart, restored.performanceRecord?.executionFlow?.get(1)?.tag) + assertEquals(EventTag.BrokerResponseSent, restored.performanceRecord?.executionFlow?.get(2)?.tag) + } + + @Test + fun telemetrySchema_serializedJson_usesExpectedFieldNames() { + val schema = TelemetrySchema(correlationId = "cid") + + val json = gson.toJson(schema) + + assert(json.contains("\"schema_version\"")) { "Expected 'schema_version' in: $json" } + assert(json.contains("\"correlation_id\"")) { "Expected 'correlation_id' in: $json" } + } +}