Browse Source

IoT Hub install report (#15838)

* feat: send pseudonymized install report (tenant/user hash, tb version) to IoT Hub

* refactor: source install-report tb version/edition from ProjectInfo

Use the existing ProjectInfo component (getProjectVersion/getProductType)
instead of an ad-hoc optional BuildProperties field and a hard-coded "CE"
edition, so PE reports its own product type and the version is the canonical
cleaned value. Constructor-injected via @RequiredArgsConstructor.
pull/15849/head
Andrew Shvayka 4 weeks ago
committed by GitHub
parent
commit
23433497ab
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 23
      application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java
  2. 19
      application/src/main/java/org/thingsboard/server/service/iot_hub/InstallReport.java
  3. 4
      application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubRestClient.java
  4. 46
      application/src/test/java/org/thingsboard/server/service/iot_hub/InstallReportTest.java

23
application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java

@ -64,6 +64,7 @@ import org.thingsboard.server.service.entitiy.dashboard.TbDashboardService;
import org.thingsboard.server.service.entitiy.device.TbDeviceService;
import org.thingsboard.server.service.entitiy.device.profile.TbDeviceProfileService;
import org.thingsboard.server.service.entitiy.widgets.type.TbWidgetTypeService;
import org.thingsboard.server.service.install.ProjectInfo;
import org.thingsboard.server.service.rule.TbRuleChainService;
import org.thingsboard.server.service.security.model.SecurityUser;
@ -112,6 +113,7 @@ public class DefaultIotHubService implements IotHubService {
private final DeviceService deviceService;
private final TbDeviceService tbDeviceService;
private final SolutionService solutionService;
private final ProjectInfo projectInfo;
// Field names of the marketplace version JSON payload. Both the install path and the
// install-plan resolver parse the same shape, so the contract lives here in one place.
@ -196,10 +198,11 @@ public class DefaultIotHubService implements IotHubService {
}
try {
iotHubRestClient.reportVersionInstalled(versionId);
InstallReport report = buildInstallReport(tenantId.getId(), user.getId().getId(),
projectInfo.getProjectVersion(), projectInfo.getProductType());
iotHubRestClient.reportVersionInstalled(versionId, report);
} catch (Exception e) {
// Counter ping is best-effort — do not fail the install if it errors.
log.warn("[{}] Failed to report install counter for version {}: {}", tenantId, versionId, e.getMessage());
log.warn("[{}] Failed to report install for version {}: {}", tenantId, versionId, e.getMessage());
}
log.info("[{}] Successfully installed IoT Hub item version: {} (type: {})", tenantId, itemName, itemType);
return installedItem;
@ -611,6 +614,13 @@ public class DefaultIotHubService implements IotHubService {
}
}
static InstallReport buildInstallReport(UUID tenantId, UUID userId, String tbVersion, String edition) {
String salt = tenantId.toString();
String tenantHash = sha256(salt + tenantId);
String userHash = sha256(salt + userId);
return new InstallReport(tenantHash, userHash, tbVersion, edition);
}
@Override
public InstallItemVersionResult registerDeviceInstall(SecurityUser user, String versionId, DeviceInstalledItemDescriptor descriptor) {
TenantId tenantId = user.getTenantId();
@ -650,10 +660,11 @@ public class DefaultIotHubService implements IotHubService {
}
try {
iotHubRestClient.reportVersionInstalled(versionId);
InstallReport report = buildInstallReport(tenantId.getId(), user.getId().getId(),
projectInfo.getProjectVersion(), projectInfo.getProductType());
iotHubRestClient.reportVersionInstalled(versionId, report);
} catch (Exception e) {
// Counter ping is best-effort — do not fail the install if it errors.
log.warn("[{}] Failed to report install counter for version {}: {}", tenantId, versionId, e.getMessage());
log.warn("[{}] Failed to report install for version {}: {}", tenantId, versionId, e.getMessage());
}
log.info("[{}] Registered device package install: {} (version {})", tenantId, itemName, version);

19
application/src/main/java/org/thingsboard/server/service/iot_hub/InstallReport.java

@ -0,0 +1,19 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.iot_hub;
public record InstallReport(String tenantHash, String userHash, String tbVersion, String edition) {
}

4
application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubRestClient.java

@ -120,9 +120,9 @@ public class IotHubRestClient {
});
}
public void reportVersionInstalled(String versionId) {
public void reportVersionInstalled(String versionId, InstallReport report) {
String url = baseUrl + "/api/versions/" + versionId + "/install";
log.debug("Reporting IoT Hub version installed: {}", url);
restTemplate.postForObject(url, null, Void.class);
restTemplate.postForObject(url, report, Void.class);
}
}

46
application/src/test/java/org/thingsboard/server/service/iot_hub/InstallReportTest.java

@ -0,0 +1,46 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.iot_hub;
import org.junit.jupiter.api.Test;
import java.security.MessageDigest;
import java.nio.charset.StandardCharsets;
import java.util.HexFormat;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
class InstallReportTest {
private static String sha256(String s) throws Exception {
MessageDigest d = MessageDigest.getInstance("SHA-256");
return HexFormat.of().formatHex(d.digest(s.getBytes(StandardCharsets.UTF_8)));
}
@Test
void buildInstallReport_hashesMatchSaltedFormula() throws Exception {
UUID tenantId = UUID.randomUUID();
UUID userId = UUID.randomUUID();
InstallReport r = DefaultIotHubService.buildInstallReport(tenantId, userId, "4.2.0", "CE");
assertEquals(sha256(tenantId.toString() + tenantId), r.tenantHash());
assertEquals(sha256(tenantId.toString() + userId), r.userHash());
assertEquals("4.2.0", r.tbVersion());
assertEquals("CE", r.edition());
}
}
Loading…
Cancel
Save