Skip to content

Commit

Permalink
Adding remote configuration for UI Load Instrumentation
Browse files Browse the repository at this point in the history
  • Loading branch information
NachoEmbrace committed Dec 18, 2024
1 parent 949d7ee commit dc48bbf
Show file tree
Hide file tree
Showing 18 changed files with 102 additions and 24 deletions.
6 changes: 5 additions & 1 deletion Sources/EmbraceConfigInternal/EmbraceConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class EmbraceConfig {

@ThreadSafe private var lastUpdateTime: TimeInterval = Date(timeIntervalSince1970: 0).timeIntervalSince1970

let configurable: EmbraceConfigurable
public let configurable: EmbraceConfigurable

let queue: DispatchableQueue

Expand Down Expand Up @@ -99,6 +99,10 @@ extension EmbraceConfig /* EmbraceConfigurable delegation */ {
return configurable.isNetworkSpansForwardingEnabled
}

public var isUiLoadInstrumentationEnabled: Bool {
return configurable.isUiLoadInstrumentationEnabled
}

public var internalLogLimits: InternalLogLimits {
return configurable.internalLogLimits
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ extension RemoteConfig: EmbraceConfigurable {

public var isNetworkSpansForwardingEnabled: Bool { isEnabled(threshold: payload.networkSpansForwardingThreshold) }

public var isUiLoadInstrumentationEnabled: Bool { isEnabled(threshold: payload.uiLoadInstrumentationThreshold) }

public var networkPayloadCaptureRules: [NetworkPayloadCaptureRule] { payload.networkPayloadCaptureRules }

public var internalLogLimits: InternalLogLimits {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public struct RemoteConfigPayload: Decodable, Equatable {
var sdkEnabledThreshold: Float
var backgroundSessionThreshold: Float
var networkSpansForwardingThreshold: Float
var uiLoadInstrumentationThreshold: Float

var internalLogsTraceLimit: Int
var internalLogsDebugLimit: Int
Expand All @@ -19,6 +20,7 @@ public struct RemoteConfigPayload: Decodable, Equatable {
var internalLogsErrorLimit: Int

var networkPayloadCaptureRules: [NetworkPayloadCaptureRule]


enum CodingKeys: String, CodingKey {
case sdkEnabledThreshold = "threshold"
Expand All @@ -33,6 +35,8 @@ public struct RemoteConfigPayload: Decodable, Equatable {
case threshold = "pct_enabled"
}

case uiLoadInstrumentationThreshold = "ui_load_instrumentation_enabled"

case internalLogLimits = "internal_log_limits"
enum InternalLogLimitsCodingKeys: String, CodingKey {
case trace
Expand Down Expand Up @@ -83,6 +87,12 @@ public struct RemoteConfigPayload: Decodable, Equatable {
networkSpansForwardingThreshold = defaultPayload.networkSpansForwardingThreshold
}

// ui load instrumentation
uiLoadInstrumentationThreshold = try rootContainer.decodeIfPresent(
Float.self,
forKey: .uiLoadInstrumentationThreshold
) ?? defaultPayload.uiLoadInstrumentationThreshold

// internal logs limit
if rootContainer.contains(.internalLogLimits) {
let internalLogsLimitsContainer = try rootContainer.nestedContainer(
Expand Down Expand Up @@ -135,6 +145,7 @@ public struct RemoteConfigPayload: Decodable, Equatable {
sdkEnabledThreshold = 100.0
backgroundSessionThreshold = 0.0
networkSpansForwardingThreshold = 0.0
uiLoadInstrumentationThreshold = 0.0

internalLogsTraceLimit = 0
internalLogsDebugLimit = 0
Expand Down
2 changes: 2 additions & 0 deletions Sources/EmbraceConfiguration/EmbraceConfigurable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import Foundation

var isNetworkSpansForwardingEnabled: Bool { get }

var isUiLoadInstrumentationEnabled: Bool { get }

var internalLogLimits: InternalLogLimits { get }

var networkPayloadCaptureRules: [NetworkPayloadCaptureRule] { get }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ public class DefaultConfig: EmbraceConfigurable {

public let isNetworkSpansForwardingEnabled: Bool = false

public let isUiLoadInstrumentationEnabled: Bool = false

public let internalLogLimits = InternalLogLimits()

public let networkPayloadCaptureRules = [NetworkPayloadCaptureRule]()
Expand Down
10 changes: 8 additions & 2 deletions Sources/EmbraceCore/Capture/CaptureServices.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import EmbraceCaptureService
import EmbraceCommonInternal
import EmbraceStorageInternal
import EmbraceUploadInternal
import EmbraceConfiguration

final class CaptureServices {

Expand All @@ -16,7 +17,11 @@ final class CaptureServices {
var context: CrashReporterContext
weak var crashReporter: CrashReporter?

init(options: Embrace.Options, storage: EmbraceStorage?, upload: EmbraceUpload?) throws {
weak var config: EmbraceConfigurable?

init(options: Embrace.Options, config: EmbraceConfigurable?, storage: EmbraceStorage?, upload: EmbraceUpload?) throws {
self.config = config

// add required capture services
// and remove duplicates
services = CaptureServiceFactory.addRequiredServices(to: options.services.unique)
Expand Down Expand Up @@ -67,7 +72,8 @@ final class CaptureServices {
}

// for testing
init(services: [CaptureService], context: CrashReporterContext) {
init(config: EmbraceConfigurable?, services: [CaptureService], context: CrashReporterContext) {
self.config = config
self.services = services
self.context = context
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ extension CaptureServices {
throw serviceNotFoundError
}

guard viewCaptureService.options.instrumentFirstRender else {
guard viewCaptureService.options.instrumentFirstRender,
config?.isUiLoadInstrumentationEnabled == true else {
throw firstRenderInstrumentationDisabledError
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public extension InstrumentableViewController {
/// - attributes: A dictionary of attributes to set on the span.
/// - Returns: An OpenTelemetry `SpanBuilder`.
/// - Throws: `ViewCaptureService.noServiceFound` if no `ViewCaptureService` is active.
/// - Throws: `ViewCaptureService.firstRenderInstrumentationDisabled` if this functionallity was not enabled when setting up the `ViewCaptureService`.
/// - Throws: `ViewCaptureService.firstRenderInstrumentationDisabled` if this functionallity was not enabled when setting up the `ViewCaptureService`, or the remote configuration for this feature was not enabled.
/// - Throws: `ViewCaptureService.parentSpanNotFound` if no parent span was found for this `UIViewController`.
/// This could mean the `UIViewController` was already rendered / deemed interactive, or the `UIViewController` has already disappeared.
func buildChildSpan(
Expand All @@ -50,7 +50,7 @@ public extension InstrumentableViewController {
/// - endTime: The end time of the span.
/// - attributes: A dictionary of attributes to set on the span.
/// - Throws: `ViewCaptureService.noServiceFound` if no `ViewCaptureService` is active.
/// - Throws: `ViewCaptureService.firstRenderInstrumentationDisabled` if this functionallity was not enabled when setting up the `ViewCaptureService`.
/// - Throws: `ViewCaptureService.firstRenderInstrumentationDisabled` if this functionallity was not enabled when setting up the `ViewCaptureService`, or the remote configuration for this feature was not enabled.
/// - Throws: `ViewCaptureService.parentSpanNotFound` if no parent span was found for this `UIViewController`.
/// This could mean the `UIViewController` was already rendered / deemed interactive, or the `UIViewController` has already disappeared.
func recordCompletedChildSpan(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public protocol InteractableViewController: UIViewController {
public extension InteractableViewController {
/// Call this method in your `UIViewController` when it is ready to be interacted by the user.
/// - Throws: `ViewCaptureService.noServiceFound` if no `ViewCaptureService` is active.
/// - Throws: `ViewCaptureService.firstRenderInstrumentationDisabled` if this functionallity was not enabled when setting up the `ViewCaptureService`.
/// - Throws: `ViewCaptureService.firstRenderInstrumentationDisabled` if this functionallity was not enabled when setting up the `ViewCaptureService`, or the remote configuration for this feature was not enabled.
func setInteractionReady() throws {
try Embrace.client?.captureServices.onInteractionReady(for: self)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ extension ViewCaptureService {
}

@objc public convenience override init() {
self.init(instrumentVisibility: true, instrumentFirstRender: false)
self.init(instrumentVisibility: true, instrumentFirstRender: true)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ public enum ViewCaptureServiceError: Error, Equatable {
/// This error means the SDK was setup without a `ViewCaptureService`.
case serviceNotFound(_ description: String)

/// This error means the `ViewCaptureService.Options` was configured with `instrumentFirstRender` as `false`.
/// This error means the `ViewCaptureService.Options` was configured with `instrumentFirstRender` as `false`,
/// or the remote configuration for this feature was not enabled.
case firstRenderInstrumentationDisabled(_ description: String)

/// This error could means the `time-to-first-render` / `time-to-interactive` span has already ended,
Expand Down
2 changes: 1 addition & 1 deletion Sources/EmbraceCore/Embrace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,8 @@ To start the SDK you first need to configure it using an `Embrace.Options` insta
self.storage = try embraceStorage ?? Embrace.createStorage(options: options)
self.deviceId = DeviceIdentifier.retrieve(from: storage)
self.upload = Embrace.createUpload(options: options, deviceId: deviceId.hex)
self.captureServices = try CaptureServices(options: options, storage: storage, upload: upload)
self.config = Embrace.createConfig(options: options, deviceId: deviceId)
self.captureServices = try CaptureServices(options: options, config: config?.configurable, storage: storage, upload: upload)
self.sessionController = SessionController(storage: storage, upload: upload, config: config)
self.sessionLifecycle = Embrace.createSessionLifecycle(controller: sessionController)
self.metadata = MetadataHandler(storage: storage, sessionController: sessionController)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,6 @@ class RemoteConfigFetcherTests: XCTestCase {

XCTAssertEqual(request.cachePolicy, .useProtocolCachePolicy)
XCTAssertEqual(request.httpMethod, "GET")

let headers = try XCTUnwrap(request.allHTTPHeaderFields)
XCTAssertEqual(request.value(forHTTPHeaderField: "Accept"), "application/json")
XCTAssertEqual(request.value(forHTTPHeaderField: "User-Agent"), TestConstants.userAgent)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class RemoteConfigPayloadTests: XCTestCase {
XCTAssertEqual(payload.sdkEnabledThreshold, 100)
XCTAssertEqual(payload.backgroundSessionThreshold, 0)
XCTAssertEqual(payload.networkSpansForwardingThreshold, 0)
XCTAssertEqual(payload.uiLoadInstrumentationThreshold, 0)
XCTAssertEqual(payload.internalLogsTraceLimit, 0)
XCTAssertEqual(payload.internalLogsDebugLimit, 0)
XCTAssertEqual(payload.internalLogsInfoLimit, 0)
Expand All @@ -40,6 +41,7 @@ class RemoteConfigPayloadTests: XCTestCase {
XCTAssertEqual(payload.sdkEnabledThreshold, 50)
XCTAssertEqual(payload.backgroundSessionThreshold, 75)
XCTAssertEqual(payload.networkSpansForwardingThreshold, 25)
XCTAssertEqual(payload.uiLoadInstrumentationThreshold, 66)
XCTAssertEqual(payload.internalLogsTraceLimit, 10)
XCTAssertEqual(payload.internalLogsDebugLimit, 20)
XCTAssertEqual(payload.internalLogsInfoLimit, 30)
Expand Down Expand Up @@ -73,6 +75,7 @@ class RemoteConfigPayloadTests: XCTestCase {
XCTAssertEqual(payload.sdkEnabledThreshold, 100)
XCTAssertEqual(payload.backgroundSessionThreshold, 0)
XCTAssertEqual(payload.networkSpansForwardingThreshold, 0)
XCTAssertEqual(payload.uiLoadInstrumentationThreshold, 0)
XCTAssertEqual(payload.internalLogsTraceLimit, 0)
XCTAssertEqual(payload.internalLogsDebugLimit, 0)
XCTAssertEqual(payload.internalLogsInfoLimit, 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"network_span_forwarding": {
"pct_enabled": 25
},
"ui_load_instrumentation_enabled": 66,
"internal_log_limits": {
"trace": 10,
"debug": 20,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ class CaptureServicesUIViewControllerTests: XCTestCase {
let enabledOptions = ViewCaptureService.Options(instrumentVisibility: true, instrumentFirstRender: true)
let disabledOptions = ViewCaptureService.Options(instrumentVisibility: false, instrumentFirstRender: false)

let enabledConfig = EditableConfig(isUiLoadInstrumentationEnabled: true)
let disabledConfig = EditableConfig(isUiLoadInstrumentationEnabled: false)

func test_onInteractionReady_noService() {
// given capture services without a ViewCaptureService
let captureServices = CaptureServices(services: [], context: context)
let captureServices = CaptureServices(config: enabledConfig, services: [], context: context)
let vc = MockViewController()

// when calling onInteractionReady it throws ViewCaptureServiceError.serviceNotFound
Expand All @@ -37,7 +40,20 @@ class CaptureServicesUIViewControllerTests: XCTestCase {
func test_onInteractionReady_instrumentationDisabled() {
// given capture services with a ViewCaptureService with disabled instrumentation
let service = ViewCaptureService(options: disabledOptions)
let captureServices = CaptureServices(services: [service], context: context)
let captureServices = CaptureServices(config: enabledConfig, services: [service], context: context)
let vc = MockViewController()

// when calling onInteractionReady it throws ViewCaptureServiceError.firstRenderInstrumentationDisabled
XCTAssertThrowsError(try captureServices.onInteractionReady(for: vc)) { error in
XCTAssert(error is ViewCaptureServiceError)
XCTAssertEqual((error as! ViewCaptureServiceError).errorCode, -2)
}
}

func test_onInteractionReady_remoteConfigDisabled() {
// given capture services with a ViewCaptureService with disabled instrumentation via remote config
let service = ViewCaptureService(options: enabledOptions)
let captureServices = CaptureServices(config: disabledConfig, services: [service], context: context)
let vc = MockViewController()

// when calling onInteractionReady it throws ViewCaptureServiceError.firstRenderInstrumentationDisabled
Expand All @@ -51,7 +67,7 @@ class CaptureServicesUIViewControllerTests: XCTestCase {
// given capture services with a ViewCaptureService
let handler = MockUIViewControllerHandler()
let service = ViewCaptureService(options: enabledOptions, lock: NSLock(), handler: handler)
let captureServices = CaptureServices(services: [service], context: context)
let captureServices = CaptureServices(config: enabledConfig, services: [service], context: context)
let vc = MockViewController()

// when calling onInteractionReady
Expand All @@ -63,7 +79,7 @@ class CaptureServicesUIViewControllerTests: XCTestCase {

func test_buildChildSpan_noService() {
// given capture services without a ViewCaptureService
let captureServices = CaptureServices(services: [], context: context)
let captureServices = CaptureServices(config: enabledConfig, services: [], context: context)
let vc = MockViewController()

// when calling buildChildSpan it throws ViewCaptureServiceError.serviceNotFound
Expand All @@ -76,7 +92,20 @@ class CaptureServicesUIViewControllerTests: XCTestCase {
func test_buildChildSpan_instrumentationDisabled() {
// given capture services with a ViewCaptureService with disabled instrumentation
let service = ViewCaptureService(options: disabledOptions)
let captureServices = CaptureServices(services: [service], context: context)
let captureServices = CaptureServices(config: enabledConfig, services: [service], context: context)
let vc = MockViewController()

// when calling buildChildSpan it throws ViewCaptureServiceError.firstRenderInstrumentationDisabled
XCTAssertThrowsError(try captureServices.buildChildSpan(for: vc, name: "test")) { error in
XCTAssert(error is ViewCaptureServiceError)
XCTAssertEqual((error as! ViewCaptureServiceError).errorCode, -2)
}
}

func test_buildChildSpan_remoteConfigDisabled() {
// given capture services with a ViewCaptureService with disabled instrumentation via remote config
let service = ViewCaptureService(options: enabledOptions)
let captureServices = CaptureServices(config: disabledConfig, services: [service], context: context)
let vc = MockViewController()

// when calling buildChildSpan it throws ViewCaptureServiceError.firstRenderInstrumentationDisabled
Expand All @@ -89,7 +118,7 @@ class CaptureServicesUIViewControllerTests: XCTestCase {
func test_buildChildSpan_noParentSpan() {
// given capture services with a ViewCaptureService with no active parent span
let service = ViewCaptureService(options: enabledOptions)
let captureServices = CaptureServices(services: [service], context: context)
let captureServices = CaptureServices(config: enabledConfig, services: [service], context: context)
let vc = MockViewController()

// when calling buildChildSpan it throws ViewCaptureServiceError.parentSpanNotFound
Expand All @@ -103,7 +132,7 @@ class CaptureServicesUIViewControllerTests: XCTestCase {
// given capture services with a ViewCaptureService with an active span
let handler = MockUIViewControllerHandler()
let service = ViewCaptureService(options: enabledOptions, lock: NSLock(), handler: handler)
let captureServices = CaptureServices(services: [service], context: context)
let captureServices = CaptureServices(config: enabledConfig, services: [service], context: context)
let vc = MockViewController()

let otel = MockEmbraceOpenTelemetry()
Expand All @@ -124,7 +153,7 @@ class CaptureServicesUIViewControllerTests: XCTestCase {

func test_recordCompletedChildSpan_noService() {
// given capture services without a ViewCaptureService
let captureServices = CaptureServices(services: [], context: context)
let captureServices = CaptureServices(config: enabledConfig, services: [], context: context)
let vc = MockViewController()

// when calling recordCompletedChildSpan it throws ViewCaptureServiceError.serviceNotFound
Expand All @@ -137,7 +166,7 @@ class CaptureServicesUIViewControllerTests: XCTestCase {
func test_recordCompletedChildSpan_instrumentationDisabled() {
// given capture services with a ViewCaptureService with disabled instrumentation
let service = ViewCaptureService(options: disabledOptions)
let captureServices = CaptureServices(services: [service], context: context)
let captureServices = CaptureServices(config: enabledConfig, services: [service], context: context)
let vc = MockViewController()

// when calling recordCompletedChildSpan it throws ViewCaptureServiceError.firstRenderInstrumentationDisabled
Expand All @@ -150,7 +179,7 @@ class CaptureServicesUIViewControllerTests: XCTestCase {
func test_recordCompletedChildSpan_noParentSpan() {
// given capture services with a ViewCaptureService with no active parent span
let service = ViewCaptureService(options: enabledOptions)
let captureServices = CaptureServices(services: [service], context: context)
let captureServices = CaptureServices(config: enabledConfig, services: [service], context: context)
let vc = MockViewController()

// when calling recordCompletedChildSpan it throws ViewCaptureServiceError.parentSpanNotFound
Expand All @@ -164,7 +193,7 @@ class CaptureServicesUIViewControllerTests: XCTestCase {
// given capture services with a ViewCaptureService with an active span
let handler = MockUIViewControllerHandler()
let service = ViewCaptureService(options: enabledOptions, lock: NSLock(), handler: handler)
let captureServices = CaptureServices(services: [service], context: context)
let captureServices = CaptureServices(config: enabledConfig, services: [service], context: context)
let vc = MockViewController()

let otel = MockEmbraceOpenTelemetry()
Expand Down
Loading

0 comments on commit dc48bbf

Please sign in to comment.