mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-04 16:22:48 +08:00
Wochenreport
This commit is contained in:
88
app/app.vue
88
app/app.vue
@@ -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>
|
||||
|
||||
27
server/models/ProductUsageSession.ts
Normal file
27
server/models/ProductUsageSession.ts
Normal 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)
|
||||
Reference in New Issue
Block a user