@mentra/bluetooth-sdk 3.2.0-dev.209 → 3.2.0-dev.210
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -8
- package/android/build.gradle +2 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +89 -90
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsHost.kt +106 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsQueue.kt +111 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTracker.kt +139 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTransport.kt +49 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +11 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2SerialResolution.kt +23 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsHostTest.kt +64 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsQueueTest.kt +86 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTrackerTest.kt +148 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTransportTest.kt +44 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/G2SerialResolutionTest.kt +25 -0
- package/build/generated/changelogCatalog.d.ts +1 -1
- package/build/generated/changelogCatalog.js +1 -1
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/GeneratedChangelogCatalog.swift +1 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/internal/BluetoothSdkAnalytics.swift +127 -76
- package/ios/Source/internal/BluetoothSdkAnalyticsHost.swift +91 -0
- package/ios/Source/internal/BluetoothSdkAnalyticsQueue.swift +117 -0
- package/ios/Source/internal/BluetoothSdkAnalyticsTracker.swift +157 -0
- package/ios/Source/internal/BluetoothSdkAnalyticsTransport.swift +13 -0
- package/ios/Source/sgcs/G2.swift +374 -359
- package/ios/Source/sgcs/G2SerialResolution.swift +23 -0
- package/ios/Tests/BluetoothSdkAnalyticsHostTests.swift +63 -0
- package/ios/Tests/BluetoothSdkAnalyticsQueueTests.swift +97 -0
- package/ios/Tests/BluetoothSdkAnalyticsTrackerTests.swift +122 -0
- package/ios/Tests/BluetoothSdkAnalyticsTransportTests.swift +36 -0
- package/ios/Tests/G2SerialResolutionTests.swift +19 -0
- package/package.json +1 -1
- package/plugin/build/analyticsProps.d.ts +9 -0
- package/plugin/build/analyticsProps.js +34 -0
- package/plugin/build/index.d.ts +8 -0
- package/plugin/build/withAndroid.js +12 -18
- package/plugin/build/withIos.d.ts +1 -0
- package/plugin/build/withIos.js +8 -18
- package/src/generated/changelogCatalog.ts +1 -1
- package/src/generated/releaseMetadata.ts +5 -5
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// The subset of glasses status the analytics decision logic needs.
|
|
4
|
+
struct AnalyticsGlassesSnapshot {
|
|
5
|
+
var connected: Bool
|
|
6
|
+
var fullyBooted: Bool
|
|
7
|
+
var model: String
|
|
8
|
+
var serialNumber: String
|
|
9
|
+
var firmwareVersion = ""
|
|
10
|
+
var besFirmwareVersion = ""
|
|
11
|
+
var mtkFirmwareVersion = ""
|
|
12
|
+
var androidVersion = ""
|
|
13
|
+
var appVersion = ""
|
|
14
|
+
var buildNumber = ""
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
struct AnalyticsEvent {
|
|
18
|
+
let name: String
|
|
19
|
+
let properties: [String: Any]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// Pure decision logic for the SDK usage events. It owns no threads and no I/O so
|
|
23
|
+
/// the connection/identification/heartbeat rules can be unit-tested directly.
|
|
24
|
+
///
|
|
25
|
+
/// Rules:
|
|
26
|
+
/// - `bluetooth_sdk_glasses_connected` once per connection. If the model is not
|
|
27
|
+
/// known yet at connect time the event waits for it, and is emitted without a
|
|
28
|
+
/// model only if the connection ends first.
|
|
29
|
+
/// - `bluetooth_sdk_glasses_identified` once per connection per serial, plus a
|
|
30
|
+
/// `glasses_heartbeat` re-emission on the first status update of each new
|
|
31
|
+
/// reporting day while still connected, so a connection spanning a week boundary
|
|
32
|
+
/// is visible in both weeks. Reporting days follow `America/Los_Angeles`, the
|
|
33
|
+
/// calendar the WAU weeks are cut on; a UTC day would miss the Sunday-evening to
|
|
34
|
+
/// Monday-morning Pacific boundary, which is one UTC day.
|
|
35
|
+
struct BluetoothSdkAnalyticsTracker {
|
|
36
|
+
static let millisPerDay: Int64 = 86_400_000
|
|
37
|
+
static let reportingZone = TimeZone(identifier: "America/Los_Angeles")!
|
|
38
|
+
|
|
39
|
+
/// Calendar day in the reporting zone, as days since the epoch of that zone's midnight.
|
|
40
|
+
static func reportingDay(epochMillis: Int64) -> Int64 {
|
|
41
|
+
let offsetMillis = Int64(reportingZone.secondsFromGMT(for: Date(timeIntervalSince1970: Double(epochMillis) / 1000))) * 1000
|
|
42
|
+
let local = epochMillis + offsetMillis
|
|
43
|
+
let day = local / millisPerDay
|
|
44
|
+
return local < 0 && local % millisPerDay != 0 ? day - 1 : day
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static func reportingDay(date: Date = Date()) -> Int64 {
|
|
48
|
+
reportingDay(epochMillis: Int64((date.timeIntervalSince1970 * 1000).rounded(.down)))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private let simulatedModel: String
|
|
52
|
+
private var lastConnected = false
|
|
53
|
+
private var connectedPendingModel = false
|
|
54
|
+
private var identifiedSerial: String?
|
|
55
|
+
private var identifiedReportingDay: Int64?
|
|
56
|
+
|
|
57
|
+
init(simulatedModel: String) {
|
|
58
|
+
self.simulatedModel = simulatedModel
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
mutating func initialize(_ snapshot: AnalyticsGlassesSnapshot, reportingDay: Int64) {
|
|
62
|
+
lastConnected = snapshot.connected
|
|
63
|
+
connectedPendingModel = false
|
|
64
|
+
// Only treat identification as already captured when a valid serial is present
|
|
65
|
+
// at init. If the glasses are connected but the serial has not arrived yet
|
|
66
|
+
// (Mentra Live fills it via version_info after connect), leave this nil so the
|
|
67
|
+
// identify event still fires once the serial arrives.
|
|
68
|
+
identifiedSerial = snapshot.connected ? snapshot.serialNumber.validManufacturingSerial : nil
|
|
69
|
+
identifiedReportingDay = identifiedSerial == nil ? nil : reportingDay
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
mutating func observe(_ snapshot: AnalyticsGlassesSnapshot, reportingDay: Int64) -> [AnalyticsEvent] {
|
|
73
|
+
var events: [AnalyticsEvent] = []
|
|
74
|
+
let wasConnected = lastConnected
|
|
75
|
+
lastConnected = snapshot.connected
|
|
76
|
+
|
|
77
|
+
guard snapshot.connected else {
|
|
78
|
+
if connectedPendingModel {
|
|
79
|
+
connectedPendingModel = false
|
|
80
|
+
events.append(connectedEvent(snapshot, modelUnresolved: true))
|
|
81
|
+
}
|
|
82
|
+
identifiedSerial = nil
|
|
83
|
+
identifiedReportingDay = nil
|
|
84
|
+
return events
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if !wasConnected {
|
|
88
|
+
identifiedSerial = nil
|
|
89
|
+
identifiedReportingDay = nil
|
|
90
|
+
if snapshot.model.isBlank {
|
|
91
|
+
connectedPendingModel = true
|
|
92
|
+
} else {
|
|
93
|
+
events.append(connectedEvent(snapshot, modelUnresolved: false))
|
|
94
|
+
}
|
|
95
|
+
} else if connectedPendingModel, !snapshot.model.isBlank {
|
|
96
|
+
connectedPendingModel = false
|
|
97
|
+
events.append(connectedEvent(snapshot, modelUnresolved: false))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
guard let serial = snapshot.serialNumber.validManufacturingSerial else { return events }
|
|
101
|
+
if identifiedSerial != serial {
|
|
102
|
+
identifiedSerial = serial
|
|
103
|
+
identifiedReportingDay = reportingDay
|
|
104
|
+
events.append(identifiedEvent(snapshot, serial: serial, kind: "glasses_identified"))
|
|
105
|
+
} else if identifiedReportingDay != reportingDay {
|
|
106
|
+
identifiedReportingDay = reportingDay
|
|
107
|
+
events.append(identifiedEvent(snapshot, serial: serial, kind: "glasses_heartbeat"))
|
|
108
|
+
}
|
|
109
|
+
return events
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private func connectedEvent(_ snapshot: AnalyticsGlassesSnapshot, modelUnresolved: Bool) -> AnalyticsEvent {
|
|
113
|
+
var properties: [String: Any] = [
|
|
114
|
+
"event_kind": "glasses_connected",
|
|
115
|
+
"fully_booted": snapshot.fullyBooted,
|
|
116
|
+
"glasses_is_simulated": snapshot.model == simulatedModel,
|
|
117
|
+
]
|
|
118
|
+
if !snapshot.model.isBlank { properties["glasses_model"] = snapshot.model }
|
|
119
|
+
if modelUnresolved { properties["glasses_model_unresolved"] = true }
|
|
120
|
+
return AnalyticsEvent(name: "bluetooth_sdk_glasses_connected", properties: properties)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private func identifiedEvent(_ snapshot: AnalyticsGlassesSnapshot, serial: String, kind: String) -> AnalyticsEvent {
|
|
124
|
+
var properties: [String: Any] = [
|
|
125
|
+
"event_kind": kind,
|
|
126
|
+
"fully_booted": snapshot.fullyBooted,
|
|
127
|
+
"glasses_device_id": serial,
|
|
128
|
+
"glasses_device_id_type": "manufacturing_serial",
|
|
129
|
+
"glasses_is_simulated": snapshot.model == simulatedModel,
|
|
130
|
+
]
|
|
131
|
+
if !snapshot.model.isBlank { properties["glasses_model"] = snapshot.model }
|
|
132
|
+
let software: [(String, String)] = [
|
|
133
|
+
("glasses_firmware_version", snapshot.firmwareVersion),
|
|
134
|
+
("glasses_bes_firmware_version", snapshot.besFirmwareVersion),
|
|
135
|
+
("glasses_mtk_firmware_version", snapshot.mtkFirmwareVersion),
|
|
136
|
+
("glasses_android_version", snapshot.androidVersion),
|
|
137
|
+
("glasses_app_version", snapshot.appVersion),
|
|
138
|
+
("glasses_build_number", snapshot.buildNumber),
|
|
139
|
+
]
|
|
140
|
+
for (key, value) in software where !value.isBlank {
|
|
141
|
+
properties[key] = value
|
|
142
|
+
}
|
|
143
|
+
return AnalyticsEvent(name: "bluetooth_sdk_glasses_identified", properties: properties)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
extension String {
|
|
148
|
+
var isBlank: Bool {
|
|
149
|
+
trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
var validManufacturingSerial: String? {
|
|
153
|
+
let normalized = trimmingCharacters(in: .whitespacesAndNewlines)
|
|
154
|
+
guard !normalized.isEmpty, normalized.contains(where: { $0 != "0" }) else { return nil }
|
|
155
|
+
return normalized
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Process-wide owner of analytics delivery: one serial queue and one retry file
|
|
4
|
+
/// for the whole process. Connection tracking stays per SDK instance, but two
|
|
5
|
+
/// instances can overlap briefly when the SDK is recreated, and if each owned its
|
|
6
|
+
/// own queue a drain started by the old instance could rewrite the retry file over
|
|
7
|
+
/// an event the new instance had just persisted. A single serial queue serializes
|
|
8
|
+
/// every read-modify-write of that file.
|
|
9
|
+
enum BluetoothSdkAnalyticsTransport {
|
|
10
|
+
static let queue = DispatchQueue(label: "com.mentra.bluetoothsdk.analytics.transport")
|
|
11
|
+
static let retryQueue: BluetoothSdkAnalyticsQueue? =
|
|
12
|
+
BluetoothSdkAnalyticsQueue.defaultFileURL().map { BluetoothSdkAnalyticsQueue(fileURL: $0) }
|
|
13
|
+
}
|