Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,10 @@ export default class AnalyticsCore implements GrowingAnalyticsInterface {
Plugins.onEventWroteToDisk(pst, eventScene)
if (pst.eventType == EventType.Visit) {
EventSender.sendEvent(context)
} else if (DeviceInfo.isWearable && pst.eventType == EventType.AppClosed) {
// 手表退到后台后会被系统快速冻结,定时上报大概率等不到,
// 事件会积压到下次冷启动。因此在生成 APP_CLOSED 后立即冲刷一次。
EventSender.sendEvent(context)
}
}

Expand Down
24 changes: 22 additions & 2 deletions GrowingAnalytics/src/main/ets/components/core/DeviceInfo.ets
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,23 @@ import { ConfigMode, IgnoreFields } from '../interfaces/GrowingConfig'

export const SDK_PLATFORM: string = 'HarmonyOS'

export const DEVICE_TYPE_WEARABLE: string = 'wearable'

// 手表主流分辨率为 466 x 466(圆屏取外接正方形)
const WEARABLE_DEFAULT_SCREEN_SIZE: number = 466

export default class DeviceInfo {
static defaultScreenHeight: number = 1260
static defaultScreenWidth: number = 2720
static defaultScreenHeight: number = 2720
static defaultScreenWidth: number = 1260
static defaultPlatformVersion: string = '5.0.0'
static defaultDeviceBrand: string = 'HUAWEI'
static defaultDeviceModel: string = '-'
static defaultDeviceType: string = 'phone'

// 设备形态,仅用于 SDK 内部策略(兜底值、上报时机),不随事件上报,
// 因此不受 IgnoreFields.DeviceType 约束
static isWearable: boolean = false

static deviceId: string = ''
static platform: string = ''
static platformVersion?: string = ''
Expand All @@ -59,6 +68,13 @@ export default class DeviceInfo {
static initDeviceInfo(context: GrowingContext) {
DeviceInfo.platform = SDK_PLATFORM

DeviceInfo.isWearable = niceTry(() => deviceInfo.deviceType, '') == DEVICE_TYPE_WEARABLE
if (DeviceInfo.isWearable) {
DeviceInfo.defaultScreenHeight = WEARABLE_DEFAULT_SCREEN_SIZE
DeviceInfo.defaultScreenWidth = WEARABLE_DEFAULT_SCREEN_SIZE
DeviceInfo.defaultDeviceType = DEVICE_TYPE_WEARABLE
}

if (DeviceInfo.isNotIgnoreField(context, IgnoreFields.ScreenSize)) {
let displayInfo = niceTry(() => display.getDefaultDisplaySync())
if (displayInfo) {
Expand Down Expand Up @@ -137,6 +153,10 @@ export default class DeviceInfo {
} else if (data.netCap.bearerTypes[0] == connection.NetBearType.BEARER_ETHERNET) {
LogUtil.info(() => 'NetCapability change to Ethernet')
DeviceInfo.networkState = 'WIFI' //Ethernet
} else if (data.netCap.bearerTypes[0] == connection.NetBearType.BEARER_BLUETOOTH) {
// 手表默认网络优先级为「蓝牙 > WIFI > 蜂窝」,蓝牙代理网络归为 WIFI
LogUtil.info(() => 'NetCapability change to Bluetooth')
DeviceInfo.networkState = 'WIFI' //Bluetooth
}
})
} else {
Expand Down
3 changes: 2 additions & 1 deletion GrowingAnalytics/src/main/module.json5
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"deviceTypes": [
"default",
"tablet",
"2in1"
"2in1",
"wearable"
],
"metadata": [
{
Expand Down
3 changes: 2 additions & 1 deletion GrowingToolsKit/src/main/module.json5
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"deviceTypes": [
"default",
"tablet",
"2in1"
"2in1",
"wearable"
],
"metadata": [
{
Expand Down
12 changes: 12 additions & 0 deletions build-profile.json5
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@
}
]
},
{
"name": "entry_wearable",
"srcPath": "./entry_wearable",
"targets": [
{
"name": "default",
"applyToProducts": [
"default"
]
}
]
},
{
"name": "GrowingAnalytics",
"srcPath": "./GrowingAnalytics",
Expand Down
14 changes: 14 additions & 0 deletions docs/GrowingAnalytics/core/AnalyticsCore.md
Original file line number Diff line number Diff line change
Expand Up @@ -412,10 +412,24 @@ static async writeEventToDisk<T extends Event>(
// 4. VISIT 事件立即触发发送
if (pst.eventType == EventType.Visit) {
EventSender.sendEvent(context)
} else if (DeviceInfo.isWearable && pst.eventType == EventType.AppClosed) {
// 手表:APP_CLOSED(进入后台)立即触发发送
EventSender.sendEvent(context)
}
}
```

### 立即上报的时机

除 `setInterval` 定时上报外,以下两种事件写入磁盘后会立即触发一次上报:

| 事件 | 适用设备 | 原因 |
|---|---|---|
| `VISIT` | 全部 | 保证会话起点及时到达服务端 |
| `APP_CLOSED` | 仅手表(`DeviceInfo.isWearable`) | 手表退到后台后会被系统快速冻结,定时器大概率等不到,事件会积压到下次冷启动 |

手表侧该行为不受 `dataUploadInterval` 配置影响 —— 调大间隔省电的同时,进入后台仍会冲刷一次,两者互补。手机/平板/PC 不做此处理,维持原有的定时上报节奏。

### 写入流程图

```
Expand Down
44 changes: 36 additions & 8 deletions docs/GrowingAnalytics/core/DeviceInfo.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,21 +73,43 @@ SDK 初始化
### 默认值

```typescript
static defaultScreenHeight: number = 1260
static defaultScreenWidth: number = 2720
static defaultScreenHeight: number = 2720
static defaultScreenWidth: number = 1260
static defaultPlatformVersion: string = '5.0.0'
static defaultDeviceBrand: string = 'HUAWEI'
static defaultDeviceModel: string = '-'
static defaultDeviceType: string = 'phone'
```

上表为手机形态的默认值。手表(`deviceType == 'wearable'`)在 `initDeviceInfo()` 中会覆盖其中三项:

| 字段 | 手机 | 手表 |
|---|---|---|
| `defaultScreenHeight` | 2720 | 466 |
| `defaultScreenWidth` | 1260 | 466 |
| `defaultDeviceType` | `'phone'` | `'wearable'` |

手机默认值取竖屏形态(宽 1260 × 高 2720),与 `orientation` 的默认值 `'PORTRAIT'` 保持一致。

默认值仅在 `display.getDefaultDisplaySync()` 等系统 API 取值失败时兜底,且只被圈选(`Hybrid`)与移动调试(`WebSocket`)使用,不进入事件上报字段。

此外 `DeviceInfo.isWearable` 记录设备形态,供 SDK 内部策略使用(兜底值、上报时机,见 [`AnalyticsCore.md`](./AnalyticsCore.md))。该字段不随事件上报,因此不受 `IgnoreFields.DeviceType` 约束。

### 初始化流程

```typescript
static initDeviceInfo(context: GrowingContext) {
// 1. 设置平台
DeviceInfo.platform = SDK_PLATFORM // 'HarmonyOS'

// 1.1 设备形态:手表覆盖默认值
DeviceInfo.isWearable = niceTry(() => deviceInfo.deviceType, '') == 'wearable'
if (DeviceInfo.isWearable) {
DeviceInfo.defaultScreenHeight = 466
DeviceInfo.defaultScreenWidth = 466
DeviceInfo.defaultDeviceType = 'wearable'
}

// 2. 屏幕信息
if (DeviceInfo.isNotIgnoreField(context, IgnoreFields.ScreenSize)) {
let displayInfo = niceTry(() => display.getDefaultDisplaySync())
Expand Down Expand Up @@ -345,6 +367,8 @@ static initNetworkState(context: GrowingContext) {
DeviceInfo.networkState = 'WIFI'
} else if (bearerType == connection.NetBearType.BEARER_ETHERNET) {
DeviceInfo.networkState = 'WIFI' // Ethernet 归类为 WIFI
} else if (bearerType == connection.NetBearType.BEARER_BLUETOOTH) {
DeviceInfo.networkState = 'WIFI' // 蓝牙代理网络归类为 WIFI
}
})
} else {
Expand All @@ -355,12 +379,16 @@ static initNetworkState(context: GrowingContext) {

### 网络类型映射

| 系统网络类型 | SDK 网络状态 |
|-------------|-------------|
| BEARER_CELLULAR | 5G (简化处理) |
| BEARER_WIFI | WIFI |
| BEARER_ETHERNET | WIFI |
| 无网络 | UNKNOWN |
| 系统网络类型 | SDK 网络状态 | 说明 |
|-------------|-------------|------|
| BEARER_CELLULAR | 5G | 简化处理,未细分制式 |
| BEARER_WIFI | WIFI | |
| BEARER_ETHERNET | WIFI | |
| BEARER_BLUETOOTH | WIFI | 手表默认网络优先级为「蓝牙 > WIFI > 蜂窝」,蓝牙代理网络归为 WIFI |
| 其他(含 BEARER_VPN) | 保持上一个值 | 无 else 分支,不覆盖 |
| 无网络 | UNKNOWN | |

> **已知行为**:`networkState` 初始值为 `'UNKNOWN'`,由异步的 `netCapabilitiesChange` 回调填充;而 VISIT 事件在 `initDeviceInfo()` 的同步调用栈中生成(`AnalyticsCore.startCore` → `Session.refreshSession` → `Session.generateVisit`),回调赶不上。因此**每次冷启动的首个 VISIT 事件的 `networkState` 为 `UNKNOWN`**,其后的事件均为真实值。这是权衡后接受的行为(同步查询网络状态需两次 IPC,会挤占初始化的主线程预算),不是缺陷。

### 实时更新

Expand Down
6 changes: 6 additions & 0 deletions entry_wearable/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/node_modules
/oh_modules
/.preview
/build
/.cxx
/.test
67 changes: 67 additions & 0 deletions entry_wearable/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# entry_wearable — 手表端示例应用

面向 HarmonyOS 智能手表(`deviceTypes: ["wearable"]`)的 GrowingAnalytics 接入示例。

手机端示例见 `entry` 模块。两者刻意拆开:按官方多设备工程结构,"一次开发多端部署"只在业务逻辑层与数据层复用,UI 与交互层仍需按设备分别开发。

## 与手机端 demo 的关键差异

| 维度 | entry(手机) | entry_wearable(手表) |
|---|---|---|
| 无埋点 | 开启 | **不开启** |
| `GrowingAnalytics.onWindowStageCreate` | 调用 | **不调用** |
| `autotrackEnabled` | `true` | `false` |
| `hybridAutotrackEnabled` | 默认 `true` | `false`(无 WebView 场景) |
| GrowingToolsKit 插件 | 挂载 | 不挂载(悬浮窗形态,手表放不下) |
| `dataUploadInterval` | 15s | 60s(减少网络唤醒,照顾续航) |
| PAGE 事件 | 无埋点自动产生 | **不产生**,且不新增手动接口(见下) |

`dataUploadInterval` 敢调到 60s,是因为 SDK 侧对手表做了特殊处理:**进入后台生成 `APP_CLOSED` 后会立即冲刷一次上报**(`AnalyticsCore.writeEventToDisk`,仅 `deviceType == 'wearable'` 生效)。手表落腕后会被系统快速冻结,只靠定时器大概率等不到下一次触发,事件会积压到下次冷启动。两者互补:平时低频省电,退出时保证送达。

## 如何关掉无埋点

SDK 有两道独立开关,本 demo 两道都关:

1. **`config.autotrackEnabled = false`** —— 默认值即为 `false`。它控制回调内部是否继续处理,但**不阻止监听注册**。
2. **不调用 `GrowingAnalytics.onWindowStageCreate(this, windowStage)`** —— 这才是决定性的一步。

第 2 点是关键。该方法是无埋点 UI 监听的唯一注册入口,内部会挂上 4 个 `UIObserver` 回调:

```
willClick → AutotrackClick.onWillClick
navDestinationUpdate → AutotrackPage.onPageUpdate
navDestinationSwitch → AutotrackPage.onPageUpdate
routerPageUpdate → AutotrackPage.onPageUpdate
```

只设 `autotrackEnabled = false` 而仍然调用它,这 4 个回调依然会注册,每次点击和路由跳转都会进入 SDK 回调再被开关拦下 —— 白白付出唤醒成本。手表 CPU 与续航都更紧张,不调用即零注册,是更干净的做法。

不调用不影响其余功能:手动埋点、会话管理、设备信息、事件入库与上报走的是另一条链路(`AnalyticsCore.setLifecycleCallback` 内部注册,与 `Autotrack` 无关)。

## 没有 PAGE 事件

不调用 `onWindowStageCreate` 就没有无埋点,SDK 也不再产生 PAGE 事件;公开 API 中没有手动上报页面浏览的接口(`trackFlutterPage` 仅供 Flutter 桥接使用)。手表端不打算为此新增 PAGE 接口——页面层级浅、停留短,分析价值不足以支撑一个新的公开 API。

确需页面维度可以自己发自定义事件,但要清楚它**不是 PAGE 事件**:`eventType` 为 `Custom`,进不了页面分析,属性也不会映射到 `PageEvent` 的 `path` / `title` / `timestamp` 等顶层字段。本 demo 不演示这种写法,以免被当成推荐 schema。

## 构建

```bash
/Applications/DevEco-Studio.app/Contents/tools/node/bin/node \
/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw.js \
--mode module -p module=entry_wearable@default -p product=default \
-p requiredDeviceType=wearable assembleHap \
--analyze=normal --parallel --incremental --daemon
```

产物:`entry_wearable/build/default/outputs/default/entry_wearable-default-signed.hap`

## 尚未验证

以下需要手表模拟器或真机确认,编译期查不到:

- `deviceInfo.deviceType` 实际返回值是否为 `wearable`,以及后端埋点协议是否接受该枚举
- 圆形屏幕下 `display.getDefaultDisplaySync()` 的返回值
- 弱网/息屏场景下事件入库与补发行为

demo 首页已把 `deviceType` 和 `deviceId` 直接显示在界面上,便于真机核对第一项。
25 changes: 25 additions & 0 deletions entry_wearable/build-profile.json5
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"apiType": "stageMode",
"buildOption": {
},
"buildOptionSet": [
{
"name": "release",
"arkOptions": {
"obfuscation": {
"ruleOptions": {
"enable": true,
"files": [
"./obfuscation-rules.txt"
]
}
}
},
},
],
"targets": [
{
"name": "default"
}
]
}
6 changes: 6 additions & 0 deletions entry_wearable/hvigorfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { hapTasks } from '@ohos/hvigor-ohos-plugin';

export default {
system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
plugins:[] /* Custom plugin to extend the functionality of Hvigor. */
}
23 changes: 23 additions & 0 deletions entry_wearable/obfuscation-rules.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Define project specific obfuscation rules here.
# You can include the obfuscation configuration files in the current module's build-profile.json5.
#
# For more details, see
# https://gitee.com/openharmony/arkcompiler_ets_frontend/blob/master/arkguard/README.md

# Obfuscation options:
# -disable-obfuscation: disable all obfuscations
# -enable-property-obfuscation: obfuscate the property names
# -enable-toplevel-obfuscation: obfuscate the names in the global scope
# -compact: remove unnecessary blank spaces and all line feeds
# -remove-log: remove all console.* statements
# -print-namecache: print the name cache that contains the mapping from the old names to new names
# -apply-namecache: reuse the given cache file

# Keep options:
# -keep-property-name: specifies property names that you want to keep
# -keep-global-name: specifies names that you want to keep in the global scope

-enable-property-obfuscation
-enable-toplevel-obfuscation
-enable-filename-obfuscation
-enable-export-obfuscation
12 changes: 12 additions & 0 deletions entry_wearable/oh-package.json5
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "entry_wearable",
"version": "1.0.0",
"description": "GrowingIO Analytics demo for HarmonyOS wearable (watch).",
"main": "",
"author": "",
"license": "",
"dependencies": {
"@growingio/analytics": "file:../GrowingAnalytics"
},
"devDependencies": {}
}
48 changes: 48 additions & 0 deletions entry_wearable/src/main/ets/entryability/EntryAbility.ets
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import UIAbility from '@ohos.app.ability.UIAbility'
import hilog from '@ohos.hilog'
import window from '@ohos.window'
import Want from '@ohos.app.ability.Want'
import AbilityConstant from '@ohos.app.ability.AbilityConstant'

export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(0x0000, 'growingWear', '%{public}s', 'Ability onCreate')
}

onDestroy(): void {
hilog.info(0x0000, 'growingWear', '%{public}s', 'Ability onDestroy')
}

onWindowStageCreate(windowStage: window.WindowStage): void {
hilog.info(0x0000, 'growingWear', '%{public}s', 'Ability onWindowStageCreate')

windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(0x0000, 'growingWear', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '')
return
}
hilog.info(0x0000, 'growingWear', 'Succeeded in loading the content.')

// 手表端不采集无埋点,因此刻意不调用:
// GrowingAnalytics.onWindowStageCreate(this, windowStage)
//
// 该方法是无埋点 UI 监听的唯一注册入口,内部会挂上 4 个 UIObserver 回调
// (willClick / navDestinationUpdate / navDestinationSwitch / routerPageUpdate)。
// 不调用即零注册,每次点击与路由跳转都不会进入 SDK 回调。
//
// 手动埋点、会话、设备信息、事件入库与上报均不依赖此方法,功能不受影响。
})
}

onWindowStageDestroy(): void {
hilog.info(0x0000, 'growingWear', '%{public}s', 'Ability onWindowStageDestroy')
}

onForeground(): void {
hilog.info(0x0000, 'growingWear', '%{public}s', 'Ability onForeground')
}

onBackground(): void {
hilog.info(0x0000, 'growingWear', '%{public}s', 'Ability onBackground')
}
}
Loading
Loading