refactor(ui): posthog as composable and option for ui telemetry

relate to kestra-io/kestra-ee#4831
This commit is contained in:
Ludovic DEHON
2025-08-29 19:36:58 +02:00
parent aed055dcb1
commit b45c0b13be
5 changed files with 105 additions and 108 deletions

View File

@@ -235,6 +235,9 @@ kestra:
traces:
root: DISABLED
ui-anonymous-usage-report:
enabled: true
anonymous-usage-report:
enabled: true
uri: https://api.kestra.io/v1/reports/server-events

View File

@@ -17,7 +17,6 @@
import VueTour from "./components/onboarding/VueTour.vue";
import DefaultLayout from "override/components/layout/DefaultLayout.vue";
import DocIdDisplay from "./components/DocIdDisplay.vue";
import posthog from "posthog-js";
import "@kestra-io/ui-libs/style.css";
import {useApiStore} from "./stores/api";
@@ -25,6 +24,7 @@
import {useLayoutStore} from "./stores/layout";
import {useCoreStore} from "./stores/core";
import {useDocStore} from "./stores/doc";
import {initPostHogForSetup} from "./composables/usePosthog";
import {useMiscStore} from "override/stores/misc";
import {useExecutionsStore} from "./stores/executions";
import * as BasicAuth from "./utils/basicAuth";
@@ -116,63 +116,10 @@
uid: uid,
});
const apiConfig = await this.apiStore.loadConfig();
this.initStats(apiConfig, config, uid);
await initPostHogForSetup(config);
return config;
},
initStats(apiConfig, config, uid) {
if (this.miscStore.configs["isAnonymousUsageEnabled"] === false) {
return;
}
// only run posthog in production
if (import.meta.env.MODE === "production") {
posthog.init(
apiConfig.posthog.token,
{
api_host: apiConfig.posthog.apiHost,
ui_host: "https://eu.posthog.com",
capture_pageview: false,
capture_pageleave: true,
autocapture: false,
}
)
posthog.register_once(this.statsGlobalData(config, uid));
if (!posthog.get_property("__alias")) {
posthog.alias(apiConfig.id);
}
}
let surveyVisible = false;
window.addEventListener("PHSurveyShown", () => {
surveyVisible = true;
});
window.addEventListener("PHSurveyClosed", () => {
surveyVisible = false;
})
window.addEventListener("KestraRouterAfterEach", () => {
if (surveyVisible) {
window.dispatchEvent(new Event("PHSurveyClosed"))
surveyVisible = false;
}
})
},
statsGlobalData(config, uid) {
return {
from: "APP",
iid: config.uuid,
uid: uid,
app: {
version: config.version,
type: "OSS"
}
}
},
},
watch: {
$route: {

View File

@@ -208,7 +208,7 @@
import MailChecker from "mailchecker"
import {useMiscStore} from "override/stores/misc"
import {useSurveySkip} from "../../composables/useSurveyData"
import {initPostHogForSetup, trackSetupEvent} from "../../utils/setupPosthog"
import {initPostHogForSetup, trackSetupEvent} from "../../composables/usePosthog"
import Cogs from "vue-material-design-icons/Cogs.vue"
import AccountPlus from "vue-material-design-icons/AccountPlus.vue"
@@ -312,9 +312,9 @@
const setupConfigurationLines = computed<ConfigLine[]>(() => {
if (!setupConfiguration.value) return []
const configs = miscStore.configs
const basicAuthValue = activeStep.value >= 1 || configs?.isBasicAuthInitialized
return [
{name: "repository", icon: Database, value: setupConfiguration.value.repositoryType || "NOT SETUP"},
{name: "queue", icon: CurrentDc, value: setupConfiguration.value.queueType || "NOT SETUP"},
@@ -420,9 +420,9 @@
user_email: userFormData.value.username
}, userFormData.value)
localStorage.setItem("basicAuthUserCreated", "true")
nextStep()
} catch (error: any) {
trackSetupEvent("setup_flow:account_creation_failed", {

View File

@@ -16,17 +16,31 @@ interface UserFormData {
interface Config {
isAnonymousUsageEnabled?: boolean
isUiAnonymousUsageEnabled?: boolean
uuid?: string
version?: string
edition?: string
}
function statsGlobalData(config: Config, uid: string): any {
return {
from: "APP",
iid: config.uuid,
uid: uid,
app: {
version: config.version,
type: config.edition
}
}
}
export async function initPostHogForSetup(config: Config): Promise<void> {
try {
if (!config?.isAnonymousUsageEnabled) return
if (!config.isUiAnonymousUsageEnabled) return
const apiStore = useApiStore()
const apiConfig = await apiStore.loadConfig()
if (!apiConfig?.posthog?.token || (window as any).posthog?.__loaded) return
const uid = getUID()
@@ -42,22 +56,40 @@ export async function initPostHogForSetup(config: Config): Promise<void> {
autocapture: false,
})
posthog.register_once(statsGlobalData(config, uid));
if (!posthog.get_property("__alias")) {
posthog.alias(apiConfig.id)
}
let surveyVisible = false;
window.addEventListener("PHSurveyShown", () => {
surveyVisible = true;
});
window.addEventListener("PHSurveyClosed", () => {
surveyVisible = false;
})
window.addEventListener("KestraRouterAfterEach", () => {
if (surveyVisible) {
window.dispatchEvent(new Event("PHSurveyClosed"))
surveyVisible = false;
}
})
} catch (error) {
console.error("Failed to initialize PostHog:", error)
}
}
export function trackSetupEvent(
eventName: string,
additionalData: Record<string, any>,
eventName: string,
additionalData: Record<string, any>,
userFormData: UserFormData
): void {
const miscStore = useMiscStore()
const uid = getUID()
if (!miscStore.configs?.isAnonymousUsageEnabled || !uid) return
const userInfo = userFormData.firstName ? {

View File

@@ -44,56 +44,59 @@ import java.util.Optional;
public class MiscController {
@Inject
protected ApplicationContext applicationContext;
@Inject
VersionProvider versionProvider;
@Inject
DashboardRepositoryInterface dashboardRepository;
@Inject
ExecutionRepositoryInterface executionRepository;
@Inject
InstanceService instanceService;
@Inject
FeatureUsageReport featureUsageReport;
@Inject
BasicAuthService basicAuthService;
@Inject
Optional<TemplateRepositoryInterface> templateRepository;
@Inject
NamespaceUtils namespaceUtils;
@io.micronaut.context.annotation.Value("${kestra.anonymous-usage-report.enabled}")
protected Boolean isAnonymousUsageEnabled;
@io.micronaut.context.annotation.Value("${kestra.ui-anonymous-usage-report.enabled}")
protected Boolean isUiAnonymousUsageEnabled;
@io.micronaut.context.annotation.Value("${kestra.environment.name}")
@Nullable
protected String environmentName;
@io.micronaut.context.annotation.Value("${kestra.environment.color}")
@Nullable
protected String environmentColor;
@io.micronaut.context.annotation.Value("${kestra.url}")
@Nullable
protected String kestraUrl;
@io.micronaut.context.annotation.Value("${kestra.server.preview.initial-rows:100}")
private Integer initialPreviewRows;
@io.micronaut.context.annotation.Value("${kestra.server.preview.max-rows:5000}")
private Integer maxPreviewRows;
@io.micronaut.context.annotation.Value("${kestra.hidden-labels.prefixes:}")
private List<String> hiddenLabelsPrefixes;
@Get("/configs")
@ExecuteOn(TaskExecutors.IO)
@Operation(tags = {"Misc"}, summary = "Retrieve the instance configuration.", description = "Global endpoint available to all users.")
@@ -101,12 +104,14 @@ public class MiscController {
Configuration.ConfigurationBuilder<?, ?> builder = Configuration
.builder()
.uuid(instanceService.fetch())
.edition(Edition.OSS)
.version(versionProvider.getVersion())
.commitId(versionProvider.getRevision())
.commitDate(versionProvider.getDate())
.isCustomDashboardsEnabled(dashboardRepository.isEnabled())
.isTaskRunEnabled(executionRepository.isTaskRunEnabled())
.isAnonymousUsageEnabled(this.isAnonymousUsageEnabled)
.isUiAnonymousUsageEnabled(this.isUiAnonymousUsageEnabled)
.isTemplateEnabled(templateRepository.isPresent())
.preview(Preview.builder()
.initial(this.initialPreviewRows)
@@ -118,7 +123,7 @@ public class MiscController {
.resourceToFilters(QueryFilter.Resource.asResourceList())
.hiddenLabelsPrefixes(hiddenLabelsPrefixes)
.url(kestraUrl);
if (this.environmentName != null || this.environmentColor != null) {
builder.environment(
Environment.builder()
@@ -127,10 +132,15 @@ public class MiscController {
.build()
);
}
return builder.build();
}
public enum Edition {
OSS,
EE
}
@Get("/{tenant}/usages/all")
@ExecuteOn(TaskExecutors.IO)
@Operation(tags = {"Misc"}, summary = "Retrieve instance usage information")
@@ -142,7 +152,7 @@ public class MiscController {
.executions(event.getExecutions())
.build();
}
@Post(uri = "/{tenant}/basicAuth")
@ExecuteOn(TaskExecutors.IO)
@Operation(tags = {"Misc"}, summary = "Configure basic authentication for the instance.", description = "Sets up basic authentication credentials.")
@@ -150,73 +160,78 @@ public class MiscController {
@RequestBody(description = "") @Body BasicAuthCredentials basicAuthCredentials
) {
basicAuthService.save(basicAuthCredentials.getUid(), new BasicAuthService.BasicAuthConfiguration(basicAuthCredentials.getUsername(), basicAuthCredentials.getPassword()));
return HttpResponse.noContent();
}
@Get("/basicAuthValidationErrors")
@ExecuteOn(TaskExecutors.IO)
@Operation(tags = {"Misc"}, summary = "Retrieve the instance configuration.", description = "Global endpoint available to all users.")
public List<String> getBasicAuthConfigErrors() {
return basicAuthService.validationErrors();
}
@Getter
@NoArgsConstructor
@SuperBuilder(toBuilder = true)
public static class Configuration {
String uuid;
String version;
Edition edition;
String commitId;
ZonedDateTime commitDate;
@JsonInclude
Boolean isCustomDashboardsEnabled;
@JsonInclude
Boolean isTaskRunEnabled;
@JsonInclude
Boolean isAnonymousUsageEnabled;
@JsonInclude
Boolean isUiAnonymousUsageEnabled;
@JsonInclude
Boolean isTemplateEnabled;
Environment environment;
String url;
Preview preview;
String systemNamespace;
List<String> hiddenLabelsPrefixes;
// List of filter by component
List<QueryFilter.ResourceField> resourceToFilters;
Boolean isAiEnabled;
Boolean isBasicAuthInitialized;
}
@Value
@Builder(toBuilder = true)
public static class Environment {
String name;
String color;
}
@Value
@Builder(toBuilder = true)
public static class Preview {
Integer initial;
Integer max;
}
@Getter
@AllArgsConstructor
public static class BasicAuthCredentials {
@@ -224,7 +239,7 @@ public class MiscController {
private String username;
private String password;
}
@SuperBuilder(toBuilder = true)
@Getter
public static class ApiUsage {