Wochenreport

This commit is contained in:
itsrubberduck
2026-05-06 17:38:36 +02:00
parent fadc5a9cb3
commit d309b712f1
2 changed files with 114 additions and 1 deletions

View File

@@ -5,7 +5,7 @@
</v-app>
</template>
<script setup lang="ts">
import { computed, onMounted, watch } from 'vue';
import { computed, onBeforeUnmount, onMounted, watch } from 'vue';
import { useHotjar, useRoute, useState } from '#imports';
import { useAuthStore } from '~/stores/auth';
import { HOTJAR_LOCAL_STORAGE_KEY, useCookieConsent } from '~/composables/useCookieConsent';
@@ -26,6 +26,7 @@ const authStore = useAuthStore();
const { hasConsent, analyticsEnabled } = useCookieConsent();
const { initialize } = useHotjar();
const hotjarInitialized = useState('hotjar-initialized', () => false);
const productSession = useState<{ product: 'classroom' | 'liveatc'; path: string; startedAt: number } | null>('product-usage-session', () => null);
const showCookieBanner = computed(() => {
if (!authStore.isAuthenticated) {
@@ -76,6 +77,74 @@ const scheduleHotjarInitialization = () => {
}, HOTJAR_INIT_DELAY);
};
const resolveTrackedProduct = (path: string): 'classroom' | 'liveatc' | null => {
if (path.startsWith('/classroom')) return 'classroom';
if (path.startsWith('/pm') || path.startsWith('/copilot') || path.startsWith('/bridge')) return 'liveatc';
return null;
};
const flushProductSession = () => {
if (typeof window === 'undefined' || !productSession.value) {
return;
}
const session = productSession.value;
productSession.value = null;
const endedAt = Date.now();
const durationSeconds = Math.round((endedAt - session.startedAt) / 1000);
if (durationSeconds < 5) {
return;
}
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (authStore.accessToken) {
headers.Authorization = `Bearer ${authStore.accessToken}`;
}
fetch('/api/service/analytics/product-session', {
method: 'POST',
headers,
body: JSON.stringify({
product: session.product,
path: session.path,
durationSeconds,
startedAt: new Date(session.startedAt).toISOString(),
endedAt: new Date(endedAt).toISOString(),
}),
keepalive: true,
}).catch(() => undefined);
};
const syncProductSession = () => {
if (typeof window === 'undefined') {
return;
}
const product = authStore.isAuthenticated ? resolveTrackedProduct(route.path) : null;
const current = productSession.value;
if (current && (!product || current.product !== product || current.path !== route.path)) {
flushProductSession();
}
if (product && !productSession.value) {
productSession.value = {
product,
path: route.path,
startedAt: Date.now(),
};
}
};
const handleVisibilityChange = () => {
if (document.visibilityState === 'hidden') {
flushProductSession();
} else {
syncProductSession();
}
};
onMounted(() => {
if (typeof window === 'undefined') {
return;
@@ -111,5 +180,22 @@ onMounted(() => {
},
{ immediate: true }
);
watch(
[() => route.path, () => authStore.isAuthenticated],
() => syncProductSession(),
{ immediate: true }
);
window.addEventListener('beforeunload', flushProductSession);
document.addEventListener('visibilitychange', handleVisibilityChange);
});
onBeforeUnmount(() => {
flushProductSession();
if (typeof window !== 'undefined') {
window.removeEventListener('beforeunload', flushProductSession);
document.removeEventListener('visibilitychange', handleVisibilityChange);
}
});
</script>

View File

@@ -0,0 +1,27 @@
import mongoose from 'mongoose'
export type ProductUsageKey = 'classroom' | 'liveatc'
export interface ProductUsageSessionDocument extends mongoose.Document {
user?: mongoose.Types.ObjectId
product: ProductUsageKey
path?: string
durationSeconds: number
startedAt: Date
endedAt: Date
createdAt: Date
}
const productUsageSessionSchema = new mongoose.Schema<ProductUsageSessionDocument>({
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', index: true },
product: { type: String, enum: ['classroom', 'liveatc'], required: true, index: true },
path: { type: String, trim: true },
durationSeconds: { type: Number, required: true, min: 0 },
startedAt: { type: Date, required: true },
endedAt: { type: Date, required: true, index: true },
createdAt: { type: Date, default: () => new Date() },
})
export const ProductUsageSession =
(mongoose.models.ProductUsageSession as mongoose.Model<ProductUsageSessionDocument> | undefined) ||
mongoose.model<ProductUsageSessionDocument>('ProductUsageSession', productUsageSessionSchema)