Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

public class DocMeta {
public static final String META_FILE_NAME = "meta.json";
public static final String TRACK_ACTIVITY_URL_KEY = "trackActivityUrl";

private String id;

Expand Down Expand Up @@ -114,6 +115,10 @@ public List<String> getAllowedGroups() {
return allowedGroups;
}

public boolean hasTrackActivityUrl() {
return docMetaMap.containsKey(TRACK_ACTIVITY_URL_KEY);
}

public Map<String, Object> toMap() {
Map<String, Object> result = new HashMap<>();
result.put("id", id);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Add: `/_stats` page to display tracked visitors for internally hosted documentations
93 changes: 91 additions & 2 deletions znai-enterprise-sample-server/znai-enterprise-sample-server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import json
import csv
import uuid
from datetime import datetime
from datetime import datetime, timedelta
from collections import defaultdict
from flask import Flask, request, jsonify
from flask_cors import CORS
from slack_sdk import WebClient
Expand Down Expand Up @@ -248,6 +249,95 @@ def persist_tracking_event(event):

writer.writerow(event)

def load_tracking_events():
if not os.path.exists(TRACKING_CSV_FILE):
return []

events = []
with open(TRACKING_CSV_FILE, 'r', newline='', encoding='utf-8') as csvfile:
reader = csv.DictReader(csvfile)
for row in reader:
events.append(row)

return events

def calculate_page_stats(events, start_time=None):
page_views = defaultdict(int)
page_unique = defaultdict(set)

for event in events:
if event.get('eventType') != 'pageOpen':
continue

timestamp_str = event.get('timestamp', '')
if start_time and timestamp_str:
try:
event_time = datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
if event_time < start_time:
continue
except ValueError:
pass

page_id = event.get('pageId', '')
if not page_id:
continue

page_views[page_id] += 1

data_str = event.get('data', '{}')
try:
data = json.loads(data_str) if data_str else {}
except json.JSONDecodeError:
data = {}

visitor_id = data.get('visitorId', timestamp_str)
page_unique[page_id].add(visitor_id)

result = {}
all_pages = set(page_views.keys()) | set(page_unique.keys())
for page_id in all_pages:
result[page_id] = {
'totalViews': page_views[page_id],
'uniqueViews': len(page_unique[page_id])
}

return result

def apply_stats_multiplier(page_stats, multiplier):
result = {}
for page_id, stats in page_stats.items():
total_views = stats['totalViews'] * multiplier
result[page_id] = {
'totalViews': int(total_views),
'uniqueViews': int(total_views * 0.2)
}
return result

@app.route('/doc-stats', methods=['GET'])
def get_doc_stats():
try:
events = load_tracking_events()

now = datetime.utcnow()
week_ago = now - timedelta(days=7)

week_stats = calculate_page_stats(events, week_ago)

stats = {
'week': apply_stats_multiplier(week_stats, 1),
'month': apply_stats_multiplier(week_stats, 2),
'year': apply_stats_multiplier(week_stats, 8),
'total': apply_stats_multiplier(week_stats, 8)
}

return jsonify(stats), 200

except Exception as e:
print(f"Error getting doc stats: {type(e).__name__}: {str(e)}")
import traceback
traceback.print_exc()
return jsonify({"error": str(e)}), 500

def format_slack_message(username, question, context, page_url):
message_parts = [f"@{username} <{page_url}|asked>: {question}"]

Expand All @@ -256,7 +346,6 @@ def format_slack_message(username, question, context, page_url):

return "\n\n".join(message_parts)

# Erase tracking CSV file on server start
if os.path.exists(TRACKING_CSV_FILE):
os.remove(TRACKING_CSV_FILE)
print(f"Erased existing tracking file: {TRACKING_CSV_FILE}")
Expand Down
4 changes: 3 additions & 1 deletion znai-reactjs/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ import { jsonPresentationDemo } from "./doc-elements/json/PresentationJson.demo"
import { footnoteDemo } from "./doc-elements/footnote/Footnote.demo";
import { asciinemaDemo } from "./doc-elements/asciinema/Asciinema.demo";
import { previewConsoleOutputDemo } from "./screens/preview-change-path/PreviewConsoleOutput.demo";
import { docStatsViewDemo } from "./screens/doc-stats/DocStatsView.demo";
import { readMoreDemo } from "./doc-elements/read-more/ReadMore.demo.js";
import { snippetsResultOutputDemo } from "./doc-elements/code-snippets/SnippetResultOutput.demo.jsx";
import { createLocalSearchIndex } from "./doc-elements/search/flexSearch.ts";
Expand Down Expand Up @@ -241,7 +242,8 @@ registries
.registerAsTabs("Landing", landingDemo)
.registerAsTabs("Not Authorized", notAuthorizedDemo)
.registerAsTabs("Search Popup", searchPopupDemo)
.registerAsTabs("Preview Console Output", previewConsoleOutputDemo);
.registerAsTabs("Preview Console Output", previewConsoleOutputDemo)
.registerAsTabs("Doc Stats", docStatsViewDemo);

window.znaiSearchIdx = createLocalSearchIndex();
populateLocalSearchIndexWithData(window.znaiSearchIdx, window.znaiSearchData);
Expand Down
4 changes: 2 additions & 2 deletions znai-reactjs/src/doc-elements/Documentation.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { themeRegistry } from "../theme/ThemeRegistry";

import SearchPopup from "./search/SearchPopup";
import { getSearchPromise } from "./search/searchPromise";
import { documentationNavigation } from "../structure/DocumentationNavigation";
import { documentationNavigation, currentPageId } from "../structure/DocumentationNavigation";
import { documentationTracking } from "./tracking/DocumentationTracking";
import { tableOfContents } from "../structure/toc/TableOfContents";
import { getAllPagesPromise } from "./allPages";
Expand Down Expand Up @@ -444,7 +444,7 @@ export class Documentation extends React.Component {
document.title = page.tocItem.pageTitle ? docMeta.title + ": " + page.tocItem.pageTitle : docMeta.title;

this.setState({ presentationRegistry }, () => {
documentationTracking.onPageOpen(currentPageLocation);
documentationTracking.onPageOpen(currentPageId());
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@
*/

import React, { useEffect, useState } from "react";
import { currentPageId, pageIdFromTocItem } from "../../structure/DocumentationNavigation";
import { currentPageIdWithDocId, pageIdFromTocItem } from "../../structure/DocumentationNavigation";
import { getDocMeta } from "../../structure/docMeta";
import { Notification } from "../../components/Notification";
import { HighlightedText } from "./HighlightedText";
import { TocItem } from "../../structure/TocItem";
import { errorNotifications } from "../../components/DismissableErrorIndicators";
import { fetchWithCredentials } from "../../utils/fetchWithCredentials";

import { ResolveQuestionButton } from "./ResolveQuestionButton";
import { removeTrailingSlashFromQueryParam } from "./queryParamUtils";
Expand Down Expand Up @@ -56,18 +57,15 @@ export function SlackActiveQuestions({ containerNode, tocItem }: { containerNode

async function fetchActiveQuestions() {
try {
const pageId = currentPageId();
const pageId = currentPageIdWithDocId();
const baseUrl = getDocMeta().slackActiveQuestionsUrl;
if (!baseUrl) {
return;
}

const url = `${baseUrl}?pageId=${encodeURIComponent(pageId)}&questionId=${questionId}`;

const response = await fetch(url, {
method: "GET",
credentials: "include",
});
const response = await fetchWithCredentials(url);

if (response.ok) {
const data = await response.json();
Expand Down Expand Up @@ -102,10 +100,12 @@ export function SlackActiveQuestions({ containerNode, tocItem }: { containerNode

async function resolveQuestionPost(question: Question) {
try {
const response = await fetch(getDocMeta().resolveSlackQuestionUrl! + "/" + question.slackMessageTs, {
method: "POST",
credentials: "include",
});
const response = await fetchWithCredentials(
getDocMeta().resolveSlackQuestionUrl! + "/" + question.slackMessageTs,
{
method: "POST",
}
);

if (response.ok) {
setNotification({ type: "success", message: "Resolved slack question" });
Expand Down
15 changes: 4 additions & 11 deletions znai-reactjs/src/doc-elements/text-selection/TextSelectionMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ import { findPrefixSuffixAndMatch } from "./textSelectionBuilder";
import { buildHighlightUrl } from "./highlightUrl";

import { getDocMeta } from "../../structure/docMeta";
import { fetchWithCredentials } from "../../utils/fetchWithCredentials";

import { buildContext } from "./markdownContextBuilder";
import { Notification } from "../../components/Notification";
import { currentPageId } from "../../structure/DocumentationNavigation";
import { currentPageIdWithDocId } from "../../structure/DocumentationNavigation";
import "./TextSelectionMenu.css";

export interface TextMenuListener {
Expand Down Expand Up @@ -248,24 +249,16 @@ export function TextSelectionMenu({ containerNode }: { containerNode: HTMLDivEle
selectedText: panelData!.prefixSuffixMatch.selection,
selectedPrefix: panelData!.prefixSuffixMatch.prefix,
selectedSuffix: panelData!.prefixSuffixMatch.suffix,
pageId: currentPageId(),
pageId: currentPageIdWithDocId(),
pageOrigin: document.location.origin,
slackChannel: getDocMeta().slackChannel,
question: question,
context: panelData!.context,
};

try {
const headers = getDocMeta().sendToSlackIncludeContentType
? {
"Content-Type": "application/json",
}
: undefined;

const response = await fetch(getDocMeta().sendToSlackUrl!, {
const response = await fetchWithCredentials(getDocMeta().sendToSlackUrl!, {
method: "POST",
credentials: "include",
headers,
body: JSON.stringify(body),
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
*/

import { DocumentationTrackingListener } from "./DocumentationTracking";
import { getDocId, getDocMeta } from "../../structure/docMeta";
import { getDocId } from "../../structure/docMeta";
import { fetchWithCredentials } from "../../utils/fetchWithCredentials";

export interface TrackingEvent {
docId: string;
Expand Down Expand Up @@ -78,17 +79,9 @@ export class HttpDocumentationTracking implements DocumentationTrackingListener
};

try {
const headers = getDocMeta().trackActivityIncludeContentType
? {
"Content-Type": "application/json",
}
: undefined;

const response = await fetch(this.trackingUrl, {
const response = await fetchWithCredentials(this.trackingUrl, {
method: "POST",
headers: headers,
body: JSON.stringify(event),
credentials: "include",
});

if (!response.ok) {
Expand Down
2 changes: 2 additions & 0 deletions znai-reactjs/src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {Documentation} from "./doc-elements/Documentation";
import {DocumentationPreparationScreen} from './screens/documentation-preparation/DocumentationPreparationScreen'
import {PreviewChangeScreen} from './screens/preview-change-path/PreviewChangeScreen'
import {NotAuthorizedScreen} from './screens/not-authorized/NotAuthorizedScreen'
import {DocStatsScreen} from './screens/doc-stats/DocStatsScreen'
import {Landing} from './screens/landing/Landing'
import {themeRegistry} from './theme/ThemeRegistry'
import {documentationNavigation} from './structure/DocumentationNavigation.jsx'
Expand All @@ -42,6 +43,7 @@ window.ReactDOM = ReactDOM
window.Documentation = Documentation
window.DocumentationPreparationScreen = DocumentationPreparationScreen
window.NotAuthorizedScreen = NotAuthorizedScreen
window.DocStatsScreen = DocStatsScreen
window.Landing = Landing
window.PreviewChangeScreen = PreviewChangeScreen
window.themeRegistry = themeRegistry
Expand Down
Loading