@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.
Files changed (45) hide show
  1. package/README.md +45 -8
  2. package/android/build.gradle +2 -0
  3. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +89 -90
  4. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsHost.kt +106 -0
  5. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsQueue.kt +111 -0
  6. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTracker.kt +139 -0
  7. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTransport.kt +49 -0
  8. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +1 -1
  9. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
  10. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +11 -1
  11. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2SerialResolution.kt +23 -0
  12. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsHostTest.kt +64 -0
  13. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsQueueTest.kt +86 -0
  14. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTrackerTest.kt +148 -0
  15. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTransportTest.kt +44 -0
  16. package/android/src/test/java/com/mentra/bluetoothsdk/G2SerialResolutionTest.kt +25 -0
  17. package/build/generated/changelogCatalog.d.ts +1 -1
  18. package/build/generated/changelogCatalog.js +1 -1
  19. package/build/generated/changelogCatalog.js.map +1 -1
  20. package/build/generated/releaseMetadata.js +5 -5
  21. package/build/generated/releaseMetadata.js.map +1 -1
  22. package/ios/Source/BluetoothSdkDefaults.swift +1 -1
  23. package/ios/Source/GeneratedChangelogCatalog.swift +1 -1
  24. package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
  25. package/ios/Source/internal/BluetoothSdkAnalytics.swift +127 -76
  26. package/ios/Source/internal/BluetoothSdkAnalyticsHost.swift +91 -0
  27. package/ios/Source/internal/BluetoothSdkAnalyticsQueue.swift +117 -0
  28. package/ios/Source/internal/BluetoothSdkAnalyticsTracker.swift +157 -0
  29. package/ios/Source/internal/BluetoothSdkAnalyticsTransport.swift +13 -0
  30. package/ios/Source/sgcs/G2.swift +374 -359
  31. package/ios/Source/sgcs/G2SerialResolution.swift +23 -0
  32. package/ios/Tests/BluetoothSdkAnalyticsHostTests.swift +63 -0
  33. package/ios/Tests/BluetoothSdkAnalyticsQueueTests.swift +97 -0
  34. package/ios/Tests/BluetoothSdkAnalyticsTrackerTests.swift +122 -0
  35. package/ios/Tests/BluetoothSdkAnalyticsTransportTests.swift +36 -0
  36. package/ios/Tests/G2SerialResolutionTests.swift +19 -0
  37. package/package.json +1 -1
  38. package/plugin/build/analyticsProps.d.ts +9 -0
  39. package/plugin/build/analyticsProps.js +34 -0
  40. package/plugin/build/index.d.ts +8 -0
  41. package/plugin/build/withAndroid.js +12 -18
  42. package/plugin/build/withIos.d.ts +1 -0
  43. package/plugin/build/withIos.js +8 -18
  44. package/src/generated/changelogCatalog.ts +1 -1
  45. 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
+ }