Skip to content

Commit

Permalink
Merge pull request #1630 from DataDog/ganeshnj/feat/otel-tracer-span-…
Browse files Browse the repository at this point in the history
…links

RUM-1836 feat(otel-tracer): add support for span links
  • Loading branch information
ganeshnj authored Jan 17, 2024
2 parents 3431c6a + 8ce2edf commit 6fd9e73
Show file tree
Hide file tree
Showing 8 changed files with 290 additions and 4 deletions.
24 changes: 24 additions & 0 deletions Datadog/Datadog.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,19 @@
3C2206F62AB9DBA700DE780C /* DatadogRUM.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D29A9F3429DD84AA005C54A4 /* DatadogRUM.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
3C2206F72AB9DBB600DE780C /* DatadogTrace.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D25EE93429C4C3C300CE3839 /* DatadogTrace.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
3C2206F82AB9DBC600DE780C /* DatadogInternal.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D23039A5298D513C001A1FA3 /* DatadogInternal.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
3C32359D2B55386C000B4258 /* OTelSpanLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C32359C2B55386C000B4258 /* OTelSpanLink.swift */; };
3C32359E2B55386C000B4258 /* OTelSpanLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C32359C2B55386C000B4258 /* OTelSpanLink.swift */; };
3C3235A02B55387A000B4258 /* OTelSpanLinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C32359F2B55387A000B4258 /* OTelSpanLinkTests.swift */; };
3C3235A12B55387A000B4258 /* OTelSpanLinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C32359F2B55387A000B4258 /* OTelSpanLinkTests.swift */; };
3C394EF72AA5F49F008F48BA /* URLSessionDataDelegateSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C394EF62AA5F49F008F48BA /* URLSessionDataDelegateSwizzler.swift */; };
3C394EF82AA5F49F008F48BA /* URLSessionDataDelegateSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C394EF62AA5F49F008F48BA /* URLSessionDataDelegateSwizzler.swift */; };
3C394EFA2AA5F4C8008F48BA /* URLSessionDataDelegateSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C394EF92AA5F4C8008F48BA /* URLSessionDataDelegateSwizzlerTests.swift */; };
3C394EFB2AA5F4C8008F48BA /* URLSessionDataDelegateSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C394EF92AA5F4C8008F48BA /* URLSessionDataDelegateSwizzlerTests.swift */; };
3C41693C29FBF4D50042B9D2 /* DatadogWebViewTracking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CE119FE29F7BE0100202522 /* DatadogWebViewTracking.framework */; };
3C5D63692B55512B00FEB4BA /* OTelTraceState+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5D63682B55512B00FEB4BA /* OTelTraceState+Datadog.swift */; };
3C5D636A2B55512B00FEB4BA /* OTelTraceState+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5D63682B55512B00FEB4BA /* OTelTraceState+Datadog.swift */; };
3C5D636C2B55513500FEB4BA /* OTelTraceState+DatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5D636B2B55513500FEB4BA /* OTelTraceState+DatadogTests.swift */; };
3C5D636D2B55513500FEB4BA /* OTelTraceState+DatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5D636B2B55513500FEB4BA /* OTelTraceState+DatadogTests.swift */; };
3C6C7FDB2B45738C006F5CBC /* OpenTelemetryApi in Frameworks */ = {isa = PBXBuildFile; productRef = 3C6C7FDA2B45738C006F5CBC /* OpenTelemetryApi */; };
3C6C7FDD2B457392006F5CBC /* OpenTelemetryApi in Frameworks */ = {isa = PBXBuildFile; productRef = 3C6C7FDC2B457392006F5CBC /* OpenTelemetryApi */; };
3C6C7FE72B459AAA006F5CBC /* OTelSpan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FE02B459AAA006F5CBC /* OTelSpan.swift */; };
Expand Down Expand Up @@ -1904,8 +1912,12 @@
3C0D5DF42A5443B100446CF9 /* DataFormatTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataFormatTests.swift; sourceTree = "<group>"; };
3C1890132ABDE99200CE9E73 /* DDURLSessionInstrumentationTests+apiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "DDURLSessionInstrumentationTests+apiTests.m"; sourceTree = "<group>"; };
3C2206F22AB9CE9300DE780C /* MetaTypeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetaTypeExtensions.swift; sourceTree = "<group>"; };
3C32359C2B55386C000B4258 /* OTelSpanLink.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTelSpanLink.swift; sourceTree = "<group>"; };
3C32359F2B55387A000B4258 /* OTelSpanLinkTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTelSpanLinkTests.swift; sourceTree = "<group>"; };
3C394EF62AA5F49F008F48BA /* URLSessionDataDelegateSwizzler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionDataDelegateSwizzler.swift; sourceTree = "<group>"; };
3C394EF92AA5F4C8008F48BA /* URLSessionDataDelegateSwizzlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionDataDelegateSwizzlerTests.swift; sourceTree = "<group>"; };
3C5D63682B55512B00FEB4BA /* OTelTraceState+Datadog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OTelTraceState+Datadog.swift"; sourceTree = "<group>"; };
3C5D636B2B55513500FEB4BA /* OTelTraceState+DatadogTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OTelTraceState+DatadogTests.swift"; sourceTree = "<group>"; };
3C6C7FE02B459AAA006F5CBC /* OTelSpan.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTelSpan.swift; sourceTree = "<group>"; };
3C6C7FE12B459AAA006F5CBC /* OTelSpanBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTelSpanBuilder.swift; sourceTree = "<group>"; };
3C6C7FE22B459AAA006F5CBC /* OTelTraceId+Datadog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OTelTraceId+Datadog.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3123,6 +3135,8 @@
3C6C7FDE2B459AAA006F5CBC /* OpenTelemetry */ = {
isa = PBXGroup;
children = (
3C5D63682B55512B00FEB4BA /* OTelTraceState+Datadog.swift */,
3C32359C2B55386C000B4258 /* OTelSpanLink.swift */,
3CC6AD172B4F07DC00015B18 /* OTelAttributeValue+Datadog.swift */,
3CB012DB2B482E0400557951 /* NOPOTelSpan.swift */,
3CB012DC2B482E0400557951 /* NOPOTelSpanBuilder.swift */,
Expand All @@ -3137,6 +3151,8 @@
3C6C7FF12B459AB3006F5CBC /* OpenTelemetry */ = {
isa = PBXGroup;
children = (
3C5D636B2B55513500FEB4BA /* OTelTraceState+DatadogTests.swift */,
3C32359F2B55387A000B4258 /* OTelSpanLinkTests.swift */,
3CC6AD1A2B4F07E700015B18 /* OTelAttributeValue+DatadogTests.swift */,
3C6C7FF22B459AB3006F5CBC /* OTelSpanId+DatadogTests.swift */,
3C6C7FF32B459AB3006F5CBC /* OTelTraceId+DatadogTests.swift */,
Expand Down Expand Up @@ -8335,6 +8351,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3C5D63692B55512B00FEB4BA /* OTelTraceState+Datadog.swift in Sources */,
61A2CC3C2A44BED30000FF25 /* Tracer.swift in Sources */,
D2C1A50229C4C4CB00946C31 /* Casting.swift in Sources */,
3CC6AD182B4F07DD00015B18 /* OTelAttributeValue+Datadog.swift in Sources */,
Expand All @@ -8350,6 +8367,7 @@
D2C1A51329C4C53F00946C31 /* OTReference.swift in Sources */,
3C6C7FEF2B459AAA006F5CBC /* OTelSpanId+Datadog.swift in Sources */,
D2C1A4FB29C4C4CB00946C31 /* MessageReceivers.swift in Sources */,
3C32359D2B55386C000B4258 /* OTelSpanLink.swift in Sources */,
61A2CC362A44B0A20000FF25 /* TraceConfiguration.swift in Sources */,
61A2CC392A44B0EA0000FF25 /* Trace.swift in Sources */,
D2C1A50029C4C4CB00946C31 /* ActiveSpansPool.swift in Sources */,
Expand Down Expand Up @@ -8390,6 +8408,8 @@
3C6C7FFD2B459AF6006F5CBC /* OTelSpanTests.swift in Sources */,
619CE7612A458D66005588CB /* TraceTests.swift in Sources */,
D2C1A52029C4C75700946C31 /* DDSpanTests.swift in Sources */,
3C5D636C2B55513500FEB4BA /* OTelTraceState+DatadogTests.swift in Sources */,
3C3235A02B55387A000B4258 /* OTelSpanLinkTests.swift in Sources */,
3C6C7FFC2B459AF6006F5CBC /* OTelTraceId+DatadogTests.swift in Sources */,
D2C1A51B29C4C75700946C31 /* DDSpanContextTests.swift in Sources */,
D2C1A52729C4C7D000946C31 /* TracingFeatureMocks.swift in Sources */,
Expand Down Expand Up @@ -8535,6 +8555,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3C5D636A2B55512B00FEB4BA /* OTelTraceState+Datadog.swift in Sources */,
61A2CC3D2A44BED30000FF25 /* Tracer.swift in Sources */,
D2C1A53829C4F2DF00946C31 /* Casting.swift in Sources */,
3CC6AD192B4F07DD00015B18 /* OTelAttributeValue+Datadog.swift in Sources */,
Expand All @@ -8550,6 +8571,7 @@
D2C1A53F29C4F2DF00946C31 /* OTReference.swift in Sources */,
3C6C7FF02B459AAA006F5CBC /* OTelSpanId+Datadog.swift in Sources */,
D2C1A54129C4F2DF00946C31 /* MessageReceivers.swift in Sources */,
3C32359E2B55386C000B4258 /* OTelSpanLink.swift in Sources */,
61A2CC372A44B0A20000FF25 /* TraceConfiguration.swift in Sources */,
61A2CC3A2A44B0EA0000FF25 /* Trace.swift in Sources */,
D2C1A54229C4F2DF00946C31 /* ActiveSpansPool.swift in Sources */,
Expand Down Expand Up @@ -8590,6 +8612,8 @@
3C6C80002B459AF6006F5CBC /* OTelSpanTests.swift in Sources */,
619CE7622A458D66005588CB /* TraceTests.swift in Sources */,
D2C1A56629C4F2E800946C31 /* DDSpanTests.swift in Sources */,
3C5D636D2B55513500FEB4BA /* OTelTraceState+DatadogTests.swift in Sources */,
3C3235A12B55387A000B4258 /* OTelSpanLinkTests.swift in Sources */,
3C6C7FFF2B459AF6006F5CBC /* OTelTraceId+DatadogTests.swift in Sources */,
D2C1A56729C4F2E800946C31 /* DDSpanContextTests.swift in Sources */,
D2C1A56829C4F2E800946C31 /* TracingFeatureMocks.swift in Sources */,
Expand Down
17 changes: 17 additions & 0 deletions DatadogTrace/Sources/OpenTelemetry/OTelSpan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ internal enum DatadogTagKeys: String {
case spanKind = "span.kind"
case errorType = "error.type"
case errorMessage = "error.message"
case spanLinks = "_dd.span_links"
}

extension OpenTelemetryApi.TraceId {
/// Returns 32 character hexadecimal string representation of lower 64 bits of the trace ID.
var lowerLongHexString: String {
return String(format: "%016llx", rawLowerLong)
}
}

internal extension OpenTelemetryApi.Status {
Expand Down Expand Up @@ -38,6 +46,7 @@ internal class OTelSpan: OpenTelemetryApi.Span {
let ddSpan: DDSpan
let tracer: DatadogTracer
let queue: DispatchQueue
let spanLinks: [OTelSpanLink]

/// `isRecording` indicates whether the span is recording or not
/// and events can be added to it.
Expand Down Expand Up @@ -93,6 +102,7 @@ internal class OTelSpan: OpenTelemetryApi.Span {
parentSpanID: OpenTelemetryApi.SpanId?,
spanContext: OpenTelemetryApi.SpanContext,
spanKind: OpenTelemetryApi.SpanKind,
spanLinks: [OTelSpanLink],
startTime: Date,
tracer: DatadogTracer
) {
Expand All @@ -104,6 +114,7 @@ internal class OTelSpan: OpenTelemetryApi.Span {
self.isRecording = true
self.queue = tracer.queue
self.tracer = tracer
self.spanLinks = spanLinks
self.ddSpan = .init(
tracer: tracer,
context: .init(
Expand Down Expand Up @@ -208,6 +219,12 @@ internal class OTelSpan: OpenTelemetryApi.Span {

// SpanKind maps to the `span.kind` tag in Datadog
ddSpan.setTag(key: DatadogTagKeys.spanKind.rawValue, value: kind.rawValue)

// Datadog uses `_dd.span_links` tag to send span links
if !spanLinks.isEmpty {
ddSpan.setTag(key: DatadogTagKeys.spanLinks.rawValue, value: spanLinks)
}

ddSpan.finish(at: time)
}

Expand Down
10 changes: 6 additions & 4 deletions DatadogTrace/Sources/OpenTelemetry/OTelSpanBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ internal class OTelSpanBuilder: OpenTelemetryApi.SpanBuilder {
var startTime: Date?
var active: Bool
var parent: Parent
var spanLinks: [OTelSpanLink] = []

enum Parent {
case currentSpan
Expand Down Expand Up @@ -69,15 +70,15 @@ internal class OTelSpanBuilder: OpenTelemetryApi.SpanBuilder {
return self
}

// swiftlint:disable unavailable_function
func addLink(spanContext: OpenTelemetryApi.SpanContext) -> Self {
fatalError("Not implemented yet")
self.spanLinks.append(OTelSpanLink(context: spanContext, attributes: [:]))
return self
}

func addLink(spanContext: OpenTelemetryApi.SpanContext, attributes: [String: OpenTelemetryApi.AttributeValue]) -> Self {
fatalError("Not implemented yet")
self.spanLinks.append(OTelSpanLink(context: spanContext, attributes: attributes))
return self
}
// swiftlint:enable unavailable_function

func setSpanKind(spanKind: OpenTelemetryApi.SpanKind) -> Self {
self.spanKind = spanKind
Expand Down Expand Up @@ -122,6 +123,7 @@ internal class OTelSpanBuilder: OpenTelemetryApi.SpanBuilder {
parentSpanID: parentContext?.spanId,
spanContext: spanContext,
spanKind: spanKind,
spanLinks: spanLinks,
startTime: startTime ?? Date(),
tracer: tracer
)
Expand Down
55 changes: 55 additions & 0 deletions DatadogTrace/Sources/OpenTelemetry/OTelSpanLink.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2019-Present Datadog, Inc.
*/

import Foundation
import OpenTelemetryApi

/// Represents a span link containing a `SpanContext` and additional attributes.
internal struct OTelSpanLink: Equatable {
/// Context of the linked span.
let context: OpenTelemetryApi.SpanContext

/// Additional attributes of the linked span.
let attributes: [String: OpenTelemetryApi.AttributeValue]
}

extension OTelSpanLink: Encodable {
enum CodingKeys: String, CodingKey {
case traceId = "trace_id"
case spanId = "span_id"
case attributes = "attributes"
case traceState = "tracestate"
case traceFlags = "flags"
}

/// Encodes the span link to the following JSON format:
/// ```json
/// {
/// "trace_id": "<exactly 32 character, zero-padded lower-case hexadecimal encoded trace id>",
/// "span_id": "<exactly 16 character, zero-padded lower-case hexadecimal encoded span id>",
/// "attributes": {"key":"value", "pairs":"of", "arbitrary":"values"},
/// "dropped_attributes_count": <decimal 64 bit integer>,
/// "tracestate": "a tracestate as defined in the W3C standard",
/// "flags": <an integer representing the flags as defined in the W3C standard>
/// },
/// ```
/// - Parameter encoder: Encoder
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
let traceId = String(context.traceId.toDatadog(), representation: .hexadecimal32Chars)

try container.encode(traceId, forKey: .traceId)
try container.encode(context.spanId.hexString, forKey: .spanId)
if !attributes.isEmpty {
try container.encode(attributes.tags, forKey: .attributes)
}

if !context.traceState.entries.isEmpty {
try container.encode(context.traceState.w3c(), forKey: .traceState)
}
try container.encode(context.traceFlags.byte, forKey: .traceFlags)
}
}
18 changes: 18 additions & 0 deletions DatadogTrace/Sources/OpenTelemetry/OTelTraceState+Datadog.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2019-Present Datadog, Inc.
*/

import Foundation
import OpenTelemetryApi

extension OpenTelemetryApi.TraceState {
/// Returns the tracestate as a string as defined in the W3C standard.
/// https://www.w3.org/TR/trace-context/#tracestate-header-field-values
/// Example: rojo=00f067aa0ba902b7,congo=t61rcWkgMzE
/// - Returns: tracestate as a string
public func w3c() -> String {
return self.entries.map { "\($0.key)=\($0.value)" }.joined(separator: ",")
}
}
81 changes: 81 additions & 0 deletions DatadogTrace/Tests/OpenTelemetry/OTelSpanLinkTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2019-Present Datadog, Inc.
*/

import XCTest
import TestUtilities
import DatadogInternal
import OpenTelemetryApi

@testable import DatadogTrace

final class OTelSpanLinkTests: XCTestCase {
func testEncoder_givenAllPropertiesArePresent() throws {
let encoder = JSONEncoder()
let traceId = TraceId(idHi: 101, idLo: 102)
let spanId = SpanId(id: 103)
var traceFlags = TraceFlags()
traceFlags.setIsSampled(true)
let traceState = TraceState(
entries: [
.init(key: "foo", value: "bar")!,
.init(key: "bar", value: "baz")!
]
)!

let spanContext = OpenTelemetryApi.SpanContext.create(
traceId: traceId,
spanId: spanId,
traceFlags: traceFlags,
traceState: traceState
)
let attributes: [String: OpenTelemetryApi.AttributeValue] = [
"foo": .string("bar")
]

let spanLink = OTelSpanLink(
context: spanContext,
attributes: attributes
)

let encoded = try encoder.encode(spanLink)
let decoded = try JSONDecoder().decode([String: AnyDecodable].self, from: encoded)

XCTAssertEqual(decoded["trace_id"]?.value as? String, "00000000000000000000000000000065")
XCTAssertEqual(decoded["span_id"]?.value as? String, "0000000000000067")
XCTAssertEqual(decoded["attributes"]?.value as? [String: String], ["foo": "bar"])
XCTAssertEqual(decoded["tracestate"]?.value as? String, "foo=bar,bar=baz")
XCTAssertEqual(decoded["flags"]?.value as? Int, 1)
}

func testEncoder_givenOnlyRequiredPropertiesArePresent() throws {
let encoder = JSONEncoder()
let traceId = TraceId(idHi: 101, idLo: 102)
let spanId = SpanId(id: 103)
let traceFlags = TraceFlags()
let traceState = TraceState()

let spanContext = OpenTelemetryApi.SpanContext.create(
traceId: traceId,
spanId: spanId,
traceFlags: traceFlags,
traceState: traceState
)

let spanLink = OTelSpanLink(
context: spanContext,
attributes: [:]
)

let encoded = try encoder.encode(spanLink)
let decoded = try JSONDecoder().decode([String: AnyDecodable].self, from: encoded)

XCTAssertEqual(decoded["trace_id"]?.value as? String, "00000000000000000000000000000065")
XCTAssertEqual(decoded["span_id"]?.value as? String, "0000000000000067")
XCTAssertNil(decoded["attributes"]?.value)
XCTAssertNil(decoded["tracestate"]?.value)
XCTAssertEqual(decoded["flags"]?.value as? Int, 0)
}
}
30 changes: 30 additions & 0 deletions DatadogTrace/Tests/OpenTelemetry/OTelTraceState+DatadogTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2019-Present Datadog, Inc.
*/

import XCTest
import TestUtilities
import DatadogInternal
import OpenTelemetryApi

@testable import DatadogTrace

final class OTelTraceStateDatadogTests: XCTestCase {
func testW3C_givenEmptyEntries() throws {
let traceState = TraceState(entries: [])!
XCTAssertEqual("", traceState.w3c())
}

func testW3C_givenSomeEntries() throws {
let traceState = TraceState(
entries: [
.init(key: "foo", value: "bar")!,
.init(key: "bar", value: "baz")!
]
)!

XCTAssertEqual("foo=bar,bar=baz", traceState.w3c())
}
}
Loading

0 comments on commit 6fd9e73

Please sign in to comment.