Skip to content

Commit

Permalink
feat: add rss telemetry recorder implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
guqing committed Dec 6, 2024
1 parent d90548c commit 326299f
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 17 deletions.
9 changes: 6 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
plugins {
id "com.github.node-gradle.node" version "5.0.0"
id "run.halo.plugin.devtools" version "0.0.9"
id "run.halo.plugin.devtools" version "0.4.1"
id "io.freefair.lombok" version "8.0.1"
id 'java'
}
Expand All @@ -13,13 +13,15 @@ java {
}

repositories {
mavenLocal()
maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots' }
mavenCentral()
}

dependencies {
implementation platform('run.halo.tools.platform:plugin:2.17.0-SNAPSHOT')
implementation platform('run.halo.tools.platform:plugin:2.20.11')
compileOnly 'run.halo.app:api'
compileOnly "run.halo.feed:api:0.0.2-SNAPSHOT"

testImplementation 'run.halo.app:api'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
Expand Down Expand Up @@ -54,5 +56,6 @@ build {
}

halo {
version = "2.17"
version = "2.20.11"
debug = true
}
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/run/halo/umami/BasicProp.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package run.halo.umami;

import lombok.Data;
import reactor.core.publisher.Mono;
import run.halo.app.plugin.ReactiveSettingFetcher;

@Data
public class BasicProp {
private String websiteId;
private String endpoint;
private String scriptName;
private String url;

public static Mono<BasicProp> fetch(ReactiveSettingFetcher settingFetcher) {
return settingFetcher.fetch("basic", BasicProp.class);
}
}
21 changes: 21 additions & 0 deletions src/main/java/run/halo/umami/RssTelemetryConfiguration.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package run.halo.umami;

import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import run.halo.app.infra.ExternalUrlSupplier;
import run.halo.app.plugin.ReactiveSettingFetcher;

@Configuration
@ConditionalOnClass(name = "run.halo.feed.TelemetryRecorder")
@RequiredArgsConstructor
public class RssTelemetryConfiguration {
private final ExternalUrlSupplier externalUrlSupplier;
private final ReactiveSettingFetcher settingFetcher;

@Bean
RssTelemetryUmamiRecorder rssTelemetryUmamiRecorder() {
return new RssTelemetryUmamiRecorder(settingFetcher, externalUrlSupplier);
}
}
122 changes: 122 additions & 0 deletions src/main/java/run/halo/umami/RssTelemetryUmamiRecorder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package run.halo.umami;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import run.halo.app.infra.ExternalUrlSupplier;
import run.halo.app.plugin.ReactiveSettingFetcher;
import run.halo.feed.TelemetryEventInfo;
import run.halo.feed.TelemetryRecorder;

import java.net.URL;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;

@Slf4j
@RequiredArgsConstructor
public class RssTelemetryUmamiRecorder implements TelemetryRecorder {
static final Map<String, String> LANG_TO_COUNTRY = Map.of(
"zh", "CN",
"en", "US",
"ja", "JP",
"ko", "KR",
"fr", "FR",
"de", "DE",
"es", "ES",
"it", "IT",
"ru", "RU"
);
private final WebClient webClient = WebClient.builder().build();
private final ReactiveSettingFetcher settingFetcher;
private final ExternalUrlSupplier externalUrlSupplier;

@Override
public void record(TelemetryEventInfo eventInfo) {
var propOpt = BasicProp.fetch(settingFetcher).blockOptional();
if (propOpt.isEmpty()) {
return;
}
var siteUrl = propOpt.get().getEndpoint();
var webSiteId = propOpt.get().getWebsiteId();
if (StringUtils.isBlank(siteUrl) || StringUtils.isBlank(webSiteId)) {
return;
}
// https://umami.is/docs/api/sending-stats
webClient.post()
.uri(StringUtils.removeEnd(siteUrl, "/") + "/api/send")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.header(HttpHeaders.USER_AGENT, genUserAgent())
.body(Mono.just(createBody(webSiteId, eventInfo)), Map.class)
.retrieve()
.bodyToMono(String.class)
.doOnError(e -> log.debug("Failed to send telemetry event to Umami.", e))
.block();
}

private String genUserAgent() {
// umami has bot detection to avoid filtering out page views
return "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) "
+ "Chrome/131.0.0.0 Safari/537.36";
}

private Map<String, Object> createBody(String webSiteId, TelemetryEventInfo eventInfo) {
var hostname = Optional.ofNullable(externalUrlSupplier.getRaw())
.map(URL::getHost)
.orElse(StringUtils.EMPTY);
var payload = new HashMap<>(Map.of(
"hostname", hostname,
"language", convertToLanguageTag(eventInfo.getLanguage()),
"referrer", StringUtils.defaultString(eventInfo.getReferrer()),
"screen", StringUtils.defaultString(eventInfo.getScreen()),
"title", eventInfo.getTitle(),
"url", eventInfo.getPageUrl(),
"website", webSiteId,
"tag", "RSS Telemetry"
));
if (StringUtils.isNotBlank(eventInfo.getIp())) {
payload.put("ip", eventInfo.getIp());
}
return Map.of("payload", payload, "type", "event");
}

private static String convertToLanguageTag(String languageCode) {
try {
return convertToLanguageTagInternal(languageCode);
} catch (Throwable e) {
// ignore
return Locale.CHINA.toLanguageTag();
}
}

/**
* <p>Convert language code to language code with region.</p>
*
* @param languageCode original code (such as zh or en)
* @return Standardized language code (such as zh-CN or en-US)
*/
private static String convertToLanguageTagInternal(String languageCode) {
if (languageCode == null || languageCode.isEmpty()) {
return Locale.CHINA.toLanguageTag();
}

Locale locale = Locale.forLanguageTag(languageCode);

// if locale has no country, fill it with the default country
if (locale.getCountry().isEmpty()) {
var country = LANG_TO_COUNTRY.get(locale.getLanguage());
if (country == null) {
return Locale.CHINA.toLanguageTag();
}
return locale.toLanguageTag() + "-" + country;
}

// Return the standardized language tag (such as zh-CN)
return locale.toLanguageTag();
}
}
15 changes: 3 additions & 12 deletions src/main/java/run/halo/umami/UmamiTrackerProcessor.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package run.halo.umami;

import lombok.Data;
import org.springframework.stereotype.Component;
import org.thymeleaf.context.ITemplateContext;
import org.thymeleaf.model.IModel;
Expand All @@ -22,10 +21,10 @@ public UmamiTrackerProcessor(ReactiveSettingFetcher settingFetcher) {
@Override
public Mono<Void> process(ITemplateContext context, IModel model,
IElementModelStructureHandler structureHandler) {
return settingFetcher.fetch("basic", BasicConfig.class)
.doOnNext(basicConfig -> {
return BasicProp.fetch(settingFetcher)
.doOnNext(prop -> {
final IModelFactory modelFactory = context.getModelFactory();
model.add(modelFactory.createText(trackerScript(basicConfig.getWebsiteId(), basicConfig.endpoint, basicConfig.scriptName)));
model.add(modelFactory.createText(trackerScript(prop.getWebsiteId(), prop.getEndpoint(), prop.getScriptName())));
})
.then();
}
Expand All @@ -35,12 +34,4 @@ private String trackerScript(String websiteId, String endpoint, String scriptNam
<script async defer data-website-id="%s" src="%s/%s"></script>
""".formatted(websiteId, endpoint, scriptName);
}

@Data
public static class BasicConfig {
String websiteId;
String endpoint;
String scriptName;
String url;
}
}
9 changes: 9 additions & 0 deletions src/main/resources/ext-definitions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
apiVersion: plugin.halo.run/v1alpha1
kind: ExtensionDefinition
metadata:
name: feed-rss-telemetry-umami-recorder
spec:
className: run.halo.umami.RssTelemetryUmamiRecorder
extensionPointName: feed-telemetry-recorder
displayName: "Umami RSS 访问量记录器"
description: "将 RSS 的内容访问量上报到 Umami"
5 changes: 4 additions & 1 deletion src/main/resources/plugin.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ metadata:
spec:
enabled: true
version: 1.0.0
requires: ">=2.17.0"
requires: ">=2.20.11"
author:
name: Halo
website: https://github.com/halo-dev
Expand All @@ -19,6 +19,9 @@ spec:
issues: https://github.com/halo-sigs/plugin-umami/issues
displayName: "Umami"
description: "提供对 Umami 的集成"
pluginDependencies:
# TODO Update the version of PluginFeed
PluginFeed?: ">=1.1.0"
license:
- name: "GPL-3.0"
url: "https://github.com/halo-sigs/plugin-umami/blob/main/LICENSE"

0 comments on commit 326299f

Please sign in to comment.