diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/swift-launchdarkly-observability-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/swift-launchdarkly-observability-Package.xcscheme
index 91ea3f3..fa1d749 100644
--- a/.swiftpm/xcode/xcshareddata/xcschemes/swift-launchdarkly-observability-Package.xcscheme
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/swift-launchdarkly-observability-Package.xcscheme
@@ -78,6 +78,16 @@
ReferencedContainer = "container:">
+
+
+
+
[Event] {
var events = [Event]()
+ self.generatingCanvasSize = pushedCanvasSize
for item in items {
appendEvents(item: item, events: &events)
}
@@ -56,7 +68,7 @@ actor SessionReplayEventGenerator {
fileprivate func wakeUpPlayerEvents(_ events: inout [Event], _ imageId: Int, _ timestamp: TimeInterval) {
// artificial mouse movement to wake up session replay player
events.append(Event(type: .IncrementalSnapshot,
- data: AnyEventData(EventData(source: .mouseInteraction,
+ data: AnyEventData(MouseInteractionData(source: .mouseInteraction,
type: .mouseDown,
id: imageId,
x: padding.width,
@@ -64,7 +76,7 @@ actor SessionReplayEventGenerator {
timestamp: timestamp,
_sid: nextSid))
events.append(Event(type: .IncrementalSnapshot,
- data: AnyEventData(EventData(source: .mouseInteraction,
+ data: AnyEventData(MouseInteractionData(source: .mouseInteraction,
type: .mouseUp,
id: imageId,
x: padding.width,
@@ -91,7 +103,8 @@ actor SessionReplayEventGenerator {
if let imageId,
let lastExportImage,
lastExportImage.originalWidth == exportImage.originalWidth,
- lastExportImage.originalHeight == exportImage.originalHeight {
+ lastExportImage.originalHeight == exportImage.originalHeight,
+ generatingCanvasSize < RRWebPlayerConstants.canvasBufferLimit {
events.append(drawImageEvent(exportImage: exportImage, timestamp: timestamp, imageId: imageId))
} else {
// if screen changed size we send fullSnapshot as canvas resizing might take to many hours on the server
@@ -122,14 +135,14 @@ actor SessionReplayEventGenerator {
fileprivate func appendTouchInteraction(interaction: TouchInteraction, events: inout [Event]) {
if let touchEventData: EventDataProtocol = switch interaction.kind {
case .touchDown(let point):
- EventData(source: .mouseInteraction,
+ MouseInteractionData(source: .mouseInteraction,
type: .touchStart,
id: imageId,
x: point.x + padding.width,
y: point.y + padding.height)
case .touchUp(let point):
- EventData(source: .mouseInteraction,
+ MouseInteractionData(source: .mouseInteraction,
type: .touchEnd,
id: imageId,
x: point.x + padding.width,
@@ -145,7 +158,7 @@ actor SessionReplayEventGenerator {
timeOffset: p.timestamp - interaction.timestamp) })
default:
- Optional.none
+ Optional.none
} {
let event = Event(type: .IncrementalSnapshot,
data: AnyEventData(touchEventData),
@@ -162,7 +175,6 @@ actor SessionReplayEventGenerator {
func clickEvent(interaction: TouchInteraction) -> Event? {
guard case .touchDown = interaction.kind else { return nil }
- let viewName = interaction.target?.className
let eventData = CustomEventData(tag: .click, payload: ClickPayload(
clickTarget: interaction.target?.className ?? "",
clickTextContent: interaction.target?.accessibilityIdentifier ?? "",
@@ -174,10 +186,8 @@ actor SessionReplayEventGenerator {
return event
}
-
-
func windowEvent(href: String, width: Int, height: Int, timestamp: TimeInterval) -> Event {
- let eventData = EventData(href: href, width: width, height: height)
+ let eventData = WindowData(href: href, width: width, height: height)
let event = Event(type: .Meta,
data: AnyEventData(eventData),
timestamp: timestamp,
@@ -232,7 +242,8 @@ actor SessionReplayEventGenerator {
func drawImageEvent(exportImage: ExportImage, timestamp: TimeInterval, imageId: Int) -> Event {
let clearRectCommand = ClearRect(x: 0, y: 0, width: exportImage.originalWidth, height: exportImage.originalHeight)
- let arrayBuffer = RRArrayBuffer(base64: exportImage.data.base64EncodedString())
+ let base64String = exportImage.data.base64EncodedString()
+ let arrayBuffer = RRArrayBuffer(base64: base64String)
let blob = AnyRRNode(RRBlob(data: [AnyRRNode(arrayBuffer)], type: exportImage.mimeType))
let drawImageCommand = DrawImage(image: AnyRRNode(RRImageBitmap(args: [blob])),
dx: 0,
@@ -244,10 +255,13 @@ actor SessionReplayEventGenerator {
id: imageId,
type: .mouseUp,
commands: [
- AnyCommand(clearRectCommand),
- AnyCommand(drawImageCommand)
+ AnyCommand(clearRectCommand, canvasSize: 80),
+ AnyCommand(drawImageCommand, canvasSize: base64String.count)
])
- let event = Event(type: .IncrementalSnapshot, data: AnyEventData(eventData), timestamp: timestamp, _sid: nextSid)
+ let event = Event(type: .IncrementalSnapshot,
+ data: AnyEventData(eventData),
+ timestamp: timestamp, _sid: nextSid)
+ generatingCanvasSize += eventData.canvasSize + RRWebPlayerConstants.canvasDrawEntourage
return event
}
@@ -264,27 +278,29 @@ actor SessionReplayEventGenerator {
func fullSnapshotEvent(exportImage: ExportImage, timestamp: TimeInterval) -> Event {
id = 0
- let rootNode = fullSnapshotNode(exportImage: exportImage)
- let eventData = EventData(node: rootNode)
+ let eventData = fullSnapshotData(exportImage: exportImage)
let event = Event(type: .FullSnapshot, data: AnyEventData(eventData), timestamp: timestamp, _sid: nextSid)
+ // start again counting canvasSize
+ generatingCanvasSize = eventData.canvasSize + RRWebPlayerConstants.canvasDrawEntourage
return event
}
- func fullSnapshotNode(exportImage: ExportImage) -> EventNode {
+ func fullSnapshotData(exportImage: ExportImage) -> DomData {
var rootNode = EventNode(id: nextId, type: .Document)
let htmlDocNode = EventNode(id: nextId, type: .DocumentType, name: "html")
rootNode.childNodes.append(htmlDocNode)
-
+ let base64String = exportImage.base64DataURL()
+
let htmlNode = EventNode(id: nextId, type: .Element, tagName: "html", attributes: ["lang": "en"], childNodes: [
EventNode(id: nextId, type: .Element, tagName: "head", attributes: [:]),
EventNode(id: nextId, type: .Element, tagName: "body", attributes: [:], childNodes: [
- exportImage.eventNode(id: nextId)
+ exportImage.eventNode(id: nextId, rr_dataURL: base64String)
]),
])
imageId = id
rootNode.childNodes.append(htmlNode)
- return rootNode
+ return DomData(node: rootNode, canvasSize: base64String.count)
}
private func appendFullSnapshotEvents(_ exportImage: ExportImage, _ timestamp: TimeInterval, _ events: inout [Event]) {
@@ -292,4 +308,8 @@ actor SessionReplayEventGenerator {
events.append(fullSnapshotEvent(exportImage: exportImage, timestamp: timestamp))
events.append(viewPortEvent(exportImage: exportImage, timestamp: timestamp))
}
+
+ func updatePushedCanvasSize() {
+ pushedCanvasSize = generatingCanvasSize
+ }
}
diff --git a/Sources/LaunchDarklySessionReplay/Exporter/SessionReplayExporter.swift b/Sources/LaunchDarklySessionReplay/Exporter/SessionReplayExporter.swift
index f0036dc..1cffc3d 100644
--- a/Sources/LaunchDarklySessionReplay/Exporter/SessionReplayExporter.swift
+++ b/Sources/LaunchDarklySessionReplay/Exporter/SessionReplayExporter.swift
@@ -23,7 +23,6 @@ actor SessionReplayExporter: EventExporting {
payloadId += 1
return payloadId
}
-
private var identifyPayload: IdentifyItemPayload?
init(context: SessionReplayContext,
@@ -100,7 +99,11 @@ actor SessionReplayExporter: EventExporting {
guard events.isNotEmpty else { return }
let input = PushPayloadVariables(sessionSecureId: initializedSession.secureId, payloadId: "\(nextPayloadId)", events: events)
+
try await replayApiService.pushPayload(input)
+
+ // flushes generating canvas size into pushedCanvasSize
+ await eventGenerator.updatePushedCanvasSize()
}
private func initializeSession(sessionSecureId: String) async throws -> InitializeSessionResponse {
diff --git a/Sources/LaunchDarklySessionReplay/Operations/PushPayloadOperation.swift b/Sources/LaunchDarklySessionReplay/Operations/PushPayloadOperation.swift
index d38f2bb..f8f99df 100644
--- a/Sources/LaunchDarklySessionReplay/Operations/PushPayloadOperation.swift
+++ b/Sources/LaunchDarklySessionReplay/Operations/PushPayloadOperation.swift
@@ -4,11 +4,11 @@ import Foundation
#endif
struct PushPayloadVariables: Codable {
- public struct EventsInput: Codable {
+ struct EventsInput: Codable {
var events: [Event]
}
- public struct ErrorInput: Codable {
+ struct ErrorInput: Codable {
}
diff --git a/Sources/LaunchDarklySessionReplay/RRWeb/CanvasDrawData.swift b/Sources/LaunchDarklySessionReplay/RRWeb/CanvasDrawData.swift
index 37661c3..487578f 100644
--- a/Sources/LaunchDarklySessionReplay/RRWeb/CanvasDrawData.swift
+++ b/Sources/LaunchDarklySessionReplay/RRWeb/CanvasDrawData.swift
@@ -10,6 +10,10 @@ struct CanvasDrawData: EventDataProtocol {
var id: Int
var type: MouseInteractions
var commands: [AnyCommand]
+
+ var canvasSize: Int {
+ commands.reduce(0) { $0 + $1.canvasSize }
+ }
}
enum CommandName: String, Codable {
@@ -24,6 +28,8 @@ protocol CommandPayload: Codable {
struct AnyCommand: Codable {
let value: any CommandPayload
+ // Transitional
+ let canvasSize: Int
private enum K: String, CodingKey { case property }
@@ -32,8 +38,11 @@ struct AnyCommand: Codable {
.drawImage: { try DrawImage(from: $0) }
]
- init(_ value: any CommandPayload) { self.value = value }
-
+ init(_ value: any CommandPayload, canvasSize: Int) {
+ self.value = value
+ self.canvasSize = canvasSize
+ }
+
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: K.self)
let name = try c.decode(CommandName.self, forKey: .property)
@@ -42,6 +51,7 @@ struct AnyCommand: Codable {
debugDescription: "Unknown command \(name)"))
}
self.value = try factory(decoder)
+ self.canvasSize = 0
}
func encode(to encoder: Encoder) throws {
diff --git a/Sources/LaunchDarklySessionReplay/RRWeb/DomData.swift b/Sources/LaunchDarklySessionReplay/RRWeb/DomData.swift
new file mode 100644
index 0000000..edac064
--- /dev/null
+++ b/Sources/LaunchDarklySessionReplay/RRWeb/DomData.swift
@@ -0,0 +1,53 @@
+import Foundation
+
+struct DomData: EventDataProtocol {
+ var node: EventNode
+ // Transitional
+ var canvasSize: Int
+
+ init(node: EventNode, canvasSize: Int) {
+ self.node = node
+ self.canvasSize = canvasSize
+ }
+
+ private enum CodingKeys: String, CodingKey {
+ case node
+ }
+
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ self.node = try container.decode(EventNode.self, forKey: .node)
+ self.canvasSize = 0
+ }
+
+ func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+ try container.encode(node, forKey: .node)
+ }
+}
+
+struct EventNode: Codable {
+ var type: NodeType
+ var name: String?
+ var tagName: String?
+ var attributes: [String: String]?
+ var childNodes: [EventNode]
+ var rootId: Int?
+ var id: Int?
+
+ init(id: Int? = nil,
+ rootId: Int? = nil,
+ type: NodeType,
+ name: String? = nil,
+ tagName: String? = nil,
+ attributes: [String : String]? = nil,
+ childNodes: [EventNode] = []) {
+ self.id = id
+ self.rootId = rootId
+ self.type = type
+ self.name = name
+ self.tagName = tagName
+ self.attributes = attributes
+ self.childNodes = childNodes
+ }
+}
diff --git a/Sources/LaunchDarklySessionReplay/RRWeb/Event.swift b/Sources/LaunchDarklySessionReplay/RRWeb/Event.swift
index f2dd67f..79ec9b0 100644
--- a/Sources/LaunchDarklySessionReplay/RRWeb/Event.swift
+++ b/Sources/LaunchDarklySessionReplay/RRWeb/Event.swift
@@ -6,12 +6,12 @@ struct Event: Codable {
var timestamp: Int64
var _sid: Int
- public init(type: EventType, data: AnyEventData, timestamp: TimeInterval, _sid: Int) {
+ init(type: EventType, data: AnyEventData, timestamp: TimeInterval, _sid: Int) {
self.type = type
self.data = data
self.timestamp = timestamp.milliseconds
self._sid = _sid
- }
+ }
}
protocol EventDataProtocol: Codable {
@@ -19,10 +19,12 @@ protocol EventDataProtocol: Codable {
struct AnyEventData: Codable {
let value: any EventDataProtocol
-
+
private enum ProbeKey: String, CodingKey { case source, tag }
- init(_ value: any EventDataProtocol) { self.value = value }
+ init(_ value: any EventDataProtocol) {
+ self.value = value
+ }
init(from decoder: Decoder) throws {
let probe = try decoder.container(keyedBy: ProbeKey.self)
@@ -32,7 +34,7 @@ struct AnyEventData: Codable {
} else if src == .mouseMove {
self.value = try MouseMoveEventData(from: decoder)
} else {
- self.value = try EventData(from: decoder)
+ self.value = try MouseInteractionData(from: decoder)
}
} else if let tag = try probe.decodeIfPresent(CustomDataTag.self, forKey: .tag) {
self.value = switch tag {
diff --git a/Sources/LaunchDarklySessionReplay/RRWeb/EventData.swift b/Sources/LaunchDarklySessionReplay/RRWeb/EventData.swift
deleted file mode 100644
index 763c72e..0000000
--- a/Sources/LaunchDarklySessionReplay/RRWeb/EventData.swift
+++ /dev/null
@@ -1,85 +0,0 @@
-import Foundation
-
-public struct EventData: EventDataProtocol {
- public struct Attributes: Codable {
- var id: Int?
- var attributes: [String: String]?
- }
-
- public struct Removal: Codable {
- var parentId: Int
- var id: Int
- }
-
- public struct Addition: Codable {
- var parentId: Int
- var nextId: Int??
- var node: EventNode
- }
-
- var source: IncrementalSource?
- var type: MouseInteractions?
- var texts = [String]()
- var attributes: [Attributes]?
- var href: String?
- var width: Int?
- var height: Int?
- var node: EventNode?
- var removes: [Removal]?
- var adds: [Addition]?
- var id: Int?
- var x: CGFloat?
- var y: CGFloat?
-
- public init(source: IncrementalSource? = nil,
- type: MouseInteractions? = nil,
- node: EventNode? = nil,
- href: String? = nil,
- width: Int? = nil,
- height: Int? = nil,
- attributes: [Attributes]? = nil,
- adds: [Addition]? = nil,
- removes: [Removal]? = nil,
- id: Int? = nil,
- x: CGFloat? = nil,
- y: CGFloat? = nil) {
- self.source = source
- self.type = type
- self.node = node
- self.href = href
- self.width = width
- self.height = height
- self.attributes = attributes
- self.adds = adds
- self.removes = removes
- self.id = id
- self.x = x
- self.y = y
- }
-}
-
-public struct EventNode: Codable {
- public var type: NodeType
- public var name: String?
- public var tagName: String?
- public var attributes: [String: String]?
- public var childNodes: [EventNode]
- public var rootId: Int?
- public var id: Int?
-
- public init(id: Int? = nil,
- rootId: Int? = nil,
- type: NodeType,
- name: String? = nil,
- tagName: String? = nil,
- attributes: [String : String]? = nil,
- childNodes: [EventNode] = []) {
- self.id = id
- self.rootId = rootId
- self.type = type
- self.name = name
- self.tagName = tagName
- self.attributes = attributes
- self.childNodes = childNodes
- }
-}
diff --git a/Sources/LaunchDarklySessionReplay/RRWeb/MouseInteractionData.swift b/Sources/LaunchDarklySessionReplay/RRWeb/MouseInteractionData.swift
new file mode 100644
index 0000000..ba92102
--- /dev/null
+++ b/Sources/LaunchDarklySessionReplay/RRWeb/MouseInteractionData.swift
@@ -0,0 +1,21 @@
+import Foundation
+
+struct MouseInteractionData: EventDataProtocol {
+ var source: IncrementalSource?
+ var type: MouseInteractions?
+ var id: Int?
+ var x: CGFloat?
+ var y: CGFloat?
+
+ init(source: IncrementalSource? = nil,
+ type: MouseInteractions? = nil,
+ id: Int? = nil,
+ x: CGFloat? = nil,
+ y: CGFloat? = nil) {
+ self.source = source
+ self.type = type
+ self.id = id
+ self.x = x
+ self.y = y
+ }
+}
diff --git a/Sources/LaunchDarklySessionReplay/RRWeb/MouseMoveEventData.swift b/Sources/LaunchDarklySessionReplay/RRWeb/MouseMoveEventData.swift
index 12f390d..9776584 100644
--- a/Sources/LaunchDarklySessionReplay/RRWeb/MouseMoveEventData.swift
+++ b/Sources/LaunchDarklySessionReplay/RRWeb/MouseMoveEventData.swift
@@ -1,7 +1,7 @@
import Foundation
-public struct MouseMoveEventData: EventDataProtocol {
- public struct Position: Codable {
+struct MouseMoveEventData: EventDataProtocol {
+ struct Position: Codable {
var x: Int
var y: Int
var id: Int?
diff --git a/Sources/LaunchDarklySessionReplay/RRWeb/WindowData.swift b/Sources/LaunchDarklySessionReplay/RRWeb/WindowData.swift
new file mode 100644
index 0000000..b1a35a8
--- /dev/null
+++ b/Sources/LaunchDarklySessionReplay/RRWeb/WindowData.swift
@@ -0,0 +1,13 @@
+import Foundation
+
+struct WindowData: EventDataProtocol {
+ var href: String?
+ var width: Int?
+ var height: Int?
+
+ init(href: String? = nil, width: Int? = nil, height: Int? = nil) {
+ self.href = href
+ self.width = width
+ self.height = height
+ }
+}
diff --git a/Sources/LaunchDarklySessionReplay/ScreenCapture/ExportImage.swift b/Sources/LaunchDarklySessionReplay/ScreenCapture/ExportImage.swift
index 3e505fb..8f7cd85 100644
--- a/Sources/LaunchDarklySessionReplay/ScreenCapture/ExportImage.swift
+++ b/Sources/LaunchDarklySessionReplay/ScreenCapture/ExportImage.swift
@@ -20,13 +20,13 @@ struct ExportImage: Equatable {
self.timestamp = timestamp
}
- func eventNode(id: Int, use_rr_dataURL: Bool = true) -> EventNode {
+ func eventNode(id: Int, rr_dataURL: String) -> EventNode {
EventNode(
id: id,
type: .Element,
tagName: "canvas",
attributes: [
- "rr_dataURL": asBase64PNGDataURL(),
+ "rr_dataURL": rr_dataURL,
"width": "\(originalWidth)",
"height": "\(originalHeight)"]
)
@@ -41,7 +41,7 @@ struct ExportImage: Equatable {
}
}
- func asBase64PNGDataURL() -> String {
+ func base64DataURL() -> String {
"data:\(mimeType);base64,\(data.base64EncodedString())"
}
diff --git a/TestApp/Sources/AppDelegate.swift b/TestApp/Sources/AppDelegate.swift
index 4279597..004e20c 100644
--- a/TestApp/Sources/AppDelegate.swift
+++ b/TestApp/Sources/AppDelegate.swift
@@ -36,7 +36,7 @@ final class AppDelegate: NSObject, UIApplicationDelegate {
// serviceName: "i-os-sessions",
sessionBackgroundTimeout: 3,
- autoInstrumentation: [.memory, .urlSession])),
+ )),
SessionReplay(options: .init(
isEnabled: true,
privacy: .init(
@@ -50,7 +50,6 @@ final class AppDelegate: NSObject, UIApplicationDelegate {
return config
}()
-
let context = { () -> LDContext in
var contextBuilder = LDContextBuilder(
key: "12345"
diff --git a/Tests/SessionReplayTests/SessionReplayEventGeneratorTests.swift b/Tests/SessionReplayTests/SessionReplayEventGeneratorTests.swift
new file mode 100644
index 0000000..aa01cdf
--- /dev/null
+++ b/Tests/SessionReplayTests/SessionReplayEventGeneratorTests.swift
@@ -0,0 +1,81 @@
+import Testing
+@testable import LaunchDarklySessionReplay
+import LaunchDarklyObservability
+import OSLog
+import CoreGraphics
+
+struct SessionReplayEventGeneratorTests {
+
+ private func makeExportImage(dataSize: Int, width: Int, height: Int, timestamp: TimeInterval) -> ExportImage {
+ let data = Data(count: dataSize)
+ return ExportImage(
+ data: data,
+ originalWidth: width,
+ originalHeight: height,
+ scale: 1.0,
+ format: .png,
+ timestamp: timestamp
+ )
+ }
+
+ @Test("Appends draw image event when same size and below limit")
+ func appendsDrawImageEventWhenSameSizeAndBelowLimit() async {
+ // Arrange
+ let generator = SessionReplayEventGenerator(
+ log: OSLog(subsystem: "test", category: "test"),
+ title: "Test"
+ )
+
+ // First image triggers full snapshot (sets imageId and lastExportImage)
+ let firstImage = makeExportImage(dataSize: 128, width: 320, height: 480, timestamp: 1.0)
+ // Second image has same dimensions but different data -> should append drawImageEvent branch
+ let secondImage = makeExportImage(dataSize: 256, width: 320, height: 480, timestamp: 2.0)
+
+ let items: [EventQueueItem] = [
+ EventQueueItem(payload: ImageItemPayload(exportImage: firstImage)),
+ EventQueueItem(payload: ImageItemPayload(exportImage: secondImage))
+ ]
+
+ // Act
+ let events = await generator.generateEvents(items: items)
+
+ // Assert
+ #expect(events.count == 4) // window/meta + fullSnapshot + viewport + drawImage
+ #expect(events[0].type == .Meta)
+ #expect(events[1].type == .FullSnapshot)
+ #expect(events[2].type == .Custom)
+ #expect(events[3].type == .IncrementalSnapshot) // drawImageEvent
+ }
+
+ @Test("Appends full snapshot when canvas buffer limit exceeded")
+ func appendsFullSnapshotWhenCanvasBufferLimitExceeded() async {
+ // Arrange
+ let generator = SessionReplayEventGenerator(
+ log: OSLog(subsystem: "test", category: "test"),
+ title: "Test"
+ )
+
+ // Choose a first image whose base64 string length will exceed the canvasBufferLimit (~10MB)
+ // Base64 inflates ~4/3, so ~8MB raw data is sufficient.
+ let largeFirstImage = makeExportImage(dataSize: 8_000_000, width: 320, height: 480, timestamp: 1.0)
+ let secondImageSameSize = makeExportImage(dataSize: 256, width: 320, height: 480, timestamp: 2.0)
+
+ let items: [EventQueueItem] = [
+ EventQueueItem(payload: ImageItemPayload(exportImage: largeFirstImage)),
+ EventQueueItem(payload: ImageItemPayload(exportImage: secondImageSameSize))
+ ]
+
+ // Act
+ let events = await generator.generateEvents(items: items)
+
+ // Assert
+ #expect(events.count == 6) // two full snapshots (window/meta + fullSnapshot + viewport)
+ #expect(events[0].type == .Meta)
+ #expect(events[1].type == .FullSnapshot)
+ #expect(events[2].type == .Custom)
+ #expect(events[3].type == .Meta)
+ #expect(events[4].type == .FullSnapshot)
+ #expect(events[5].type == .Custom)
+ }
+}
+