mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-02 21:03:34 +03:00
Add metrics for numerical count of total PDFs (#6737)
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.multipart.MultipartHttpServletRequest;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.service.PdfMetricsService;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class PdfMetricsInterceptor implements HandlerInterceptor {
|
||||
|
||||
private final PdfMetricsService pdfMetricsService;
|
||||
|
||||
@Override
|
||||
public void afterCompletion(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
Object handler,
|
||||
Exception ex) {
|
||||
try {
|
||||
if (!pdfMetricsService.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
if (!"POST".equalsIgnoreCase(request.getMethod()) || response.getStatus() >= 400) {
|
||||
return;
|
||||
}
|
||||
String path = request.getServletPath();
|
||||
if (path == null || path.isBlank()) {
|
||||
path = request.getRequestURI();
|
||||
}
|
||||
if (path == null || !path.contains("/api/v1/")) {
|
||||
return;
|
||||
}
|
||||
if (!(request instanceof MultipartHttpServletRequest multipart)) {
|
||||
return;
|
||||
}
|
||||
if (isFromEditor(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int fileCount = 0;
|
||||
for (List<MultipartFile> bucket : multipart.getMultiFileMap().values()) {
|
||||
fileCount += bucket.size();
|
||||
}
|
||||
if (fileCount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
pdfMetricsService.recordOperation(fileCount);
|
||||
} catch (Exception e) {
|
||||
log.debug("Failed to record PDF metrics", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Editor traffic carries X-Browser-Id, or (if a proxy strips it) a logged-in user's JWT.
|
||||
// JWTs start "eyJ" and have two dots; API keys do not, so they still count as API.
|
||||
private boolean isFromEditor(HttpServletRequest request) {
|
||||
String browserId = request.getHeader("X-Browser-Id");
|
||||
if (browserId != null && !browserId.isBlank()) {
|
||||
return true;
|
||||
}
|
||||
String auth = request.getHeader("Authorization");
|
||||
if (auth == null || !auth.regionMatches(true, 0, "Bearer ", 0, 7)) {
|
||||
return false;
|
||||
}
|
||||
String token = auth.substring(7).trim();
|
||||
return token.startsWith("eyJ") && token.chars().filter(c -> c == '.').count() == 2;
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import stirling.software.common.model.ApplicationProperties;
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
private final EndpointInterceptor endpointInterceptor;
|
||||
private final PdfMetricsInterceptor pdfMetricsInterceptor;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(WebMvcConfig.class);
|
||||
@@ -35,6 +36,7 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(endpointInterceptor);
|
||||
registry.addInterceptor(pdfMetricsInterceptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package stirling.software.SPDF.service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.PostHogService;
|
||||
|
||||
@Service
|
||||
public class PdfMetricsService {
|
||||
|
||||
private final PostHogService postHogService;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
private final AtomicLong operations = new AtomicLong();
|
||||
private final AtomicLong pdfs = new AtomicLong();
|
||||
private long lastOperations;
|
||||
private long lastPdfs;
|
||||
|
||||
public PdfMetricsService(
|
||||
PostHogService postHogService, ApplicationProperties applicationProperties) {
|
||||
this.postHogService = postHogService;
|
||||
this.applicationProperties = applicationProperties;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return applicationProperties.getSystem().isPosthogEnabled();
|
||||
}
|
||||
|
||||
public void recordOperation(int pdfCount) {
|
||||
if (!isEnabled()) {
|
||||
return;
|
||||
}
|
||||
operations.incrementAndGet();
|
||||
if (pdfCount > 0) {
|
||||
pdfs.addAndGet(pdfCount);
|
||||
}
|
||||
}
|
||||
|
||||
@Scheduled(fixedRate = 7200000)
|
||||
public void flushMetrics() {
|
||||
if (!isEnabled()) {
|
||||
return;
|
||||
}
|
||||
long curOps = operations.get();
|
||||
long curPdfs = pdfs.get();
|
||||
long opsDelta = curOps - lastOperations;
|
||||
long pdfsDelta = curPdfs - lastPdfs;
|
||||
if (opsDelta <= 0 && pdfsDelta <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Object> props = new HashMap<>();
|
||||
props.put("source", "api");
|
||||
props.put("operations", opsDelta);
|
||||
props.put("pdfs", pdfsDelta);
|
||||
postHogService.captureEvent("pdf_operation_metrics", props);
|
||||
|
||||
lastOperations = curOps;
|
||||
lastPdfs = curPdfs;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.multipart.MultipartHttpServletRequest;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import stirling.software.SPDF.service.PdfMetricsService;
|
||||
|
||||
class PdfMetricsInterceptorTest {
|
||||
|
||||
private PdfMetricsService service;
|
||||
private PdfMetricsInterceptor interceptor;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = mock(PdfMetricsService.class);
|
||||
when(service.isEnabled()).thenReturn(true);
|
||||
interceptor = new PdfMetricsInterceptor(service);
|
||||
}
|
||||
|
||||
private MultipartHttpServletRequest editRequest(int fileParts, String... headers) {
|
||||
MultipartHttpServletRequest request = mock(MultipartHttpServletRequest.class);
|
||||
when(request.getMethod()).thenReturn("POST");
|
||||
when(request.getServletPath()).thenReturn("/api/v1/general/rotate-pdf");
|
||||
for (int i = 0; i + 1 < headers.length; i += 2) {
|
||||
when(request.getHeader(headers[i])).thenReturn(headers[i + 1]);
|
||||
}
|
||||
MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<>();
|
||||
for (int i = 0; i < fileParts; i++) {
|
||||
files.add("fileInput", mock(MultipartFile.class));
|
||||
}
|
||||
when(request.getMultiFileMap()).thenReturn(files);
|
||||
return request;
|
||||
}
|
||||
|
||||
private HttpServletResponse response(int status, String contentType) {
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
when(response.getStatus()).thenReturn(status);
|
||||
when(response.getContentType()).thenReturn(contentType);
|
||||
return response;
|
||||
}
|
||||
|
||||
@Test
|
||||
void apiRequestIsCounted() {
|
||||
interceptor.afterCompletion(editRequest(1), response(200, "application/pdf"), null, null);
|
||||
verify(service).recordOperation(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void countsEveryFilePartUnderOneFieldName() {
|
||||
interceptor.afterCompletion(editRequest(3), response(200, "application/pdf"), null, null);
|
||||
verify(service).recordOperation(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void countsRegardlessOfResponseType() {
|
||||
interceptor.afterCompletion(editRequest(1), response(200, "application/json"), null, null);
|
||||
verify(service).recordOperation(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void editorRequestWithBrowserIdIsNotCounted() {
|
||||
interceptor.afterCompletion(
|
||||
editRequest(1, "X-Browser-Id", "abc-123"),
|
||||
response(200, "application/pdf"),
|
||||
null,
|
||||
null);
|
||||
verify(service, never()).recordOperation(anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void editorJwtWithoutBrowserIdIsNotCounted() {
|
||||
interceptor.afterCompletion(
|
||||
editRequest(1, "Authorization", "Bearer eyJhbG.eyJzdWI.sig"),
|
||||
response(200, "application/pdf"),
|
||||
null,
|
||||
null);
|
||||
verify(service, never()).recordOperation(anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void bearerApiKeyIsCounted() {
|
||||
interceptor.afterCompletion(
|
||||
editRequest(1, "Authorization", "Bearer sk-not-a-jwt-key"),
|
||||
response(200, "application/pdf"),
|
||||
null,
|
||||
null);
|
||||
verify(service).recordOperation(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void errorResponseIsNotCounted() {
|
||||
interceptor.afterCompletion(editRequest(1), response(500, "application/pdf"), null, null);
|
||||
verify(service, never()).recordOperation(anyInt());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package stirling.software.SPDF.service;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.anyMap;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.PostHogService;
|
||||
|
||||
class PdfMetricsServiceTest {
|
||||
|
||||
private PostHogService postHogService;
|
||||
private ApplicationProperties applicationProperties;
|
||||
private PdfMetricsService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
postHogService = mock(PostHogService.class);
|
||||
applicationProperties = new ApplicationProperties();
|
||||
applicationProperties.getSystem().setEnableAnalytics(true);
|
||||
service = new PdfMetricsService(postHogService, applicationProperties);
|
||||
}
|
||||
|
||||
@Test
|
||||
void flushesOperationAndPdfCounts() {
|
||||
service.recordOperation(1);
|
||||
service.recordOperation(2);
|
||||
|
||||
service.flushMetrics();
|
||||
|
||||
Map<String, Object> event = captureEvent();
|
||||
assertEquals("api", event.get("source"));
|
||||
assertEquals(2L, event.get("operations"));
|
||||
assertEquals(3L, event.get("pdfs"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sendsOnlyDeltasBetweenFlushes() {
|
||||
service.recordOperation(1);
|
||||
service.flushMetrics();
|
||||
reset(postHogService);
|
||||
|
||||
service.flushMetrics();
|
||||
verify(postHogService, never()).captureEvent(eq("pdf_operation_metrics"), anyMap());
|
||||
|
||||
service.recordOperation(2);
|
||||
service.flushMetrics();
|
||||
|
||||
Map<String, Object> event = captureEvent();
|
||||
assertEquals(1L, event.get("operations"));
|
||||
assertEquals(2L, event.get("pdfs"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNothingWhenAnalyticsDisabled() {
|
||||
applicationProperties.getSystem().setEnableAnalytics(false);
|
||||
|
||||
service.recordOperation(1);
|
||||
service.flushMetrics();
|
||||
|
||||
verify(postHogService, never()).captureEvent(eq("pdf_operation_metrics"), anyMap());
|
||||
}
|
||||
|
||||
private Map<String, Object> captureEvent() {
|
||||
ArgumentCaptor<Map<String, Object>> captor = ArgumentCaptor.forClass(Map.class);
|
||||
verify(postHogService).captureEvent(eq("pdf_operation_metrics"), captor.capture());
|
||||
return captor.getValue();
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -202,7 +202,8 @@ public class SecurityConfiguration {
|
||||
"Origin",
|
||||
"X-API-KEY",
|
||||
"X-CSRF-TOKEN",
|
||||
"X-XSRF-TOKEN"));
|
||||
"X-XSRF-TOKEN",
|
||||
"X-Browser-Id"));
|
||||
|
||||
cfg.setExposedHeaders(
|
||||
List.of(
|
||||
|
||||
@@ -296,7 +296,8 @@ public class SupabaseSecurityConfig {
|
||||
"X-Requested-With",
|
||||
"Accept",
|
||||
"Origin",
|
||||
"X-API-KEY"));
|
||||
"X-API-KEY",
|
||||
"X-Browser-Id"));
|
||||
cfg.setExposedHeaders(List.of("WWW-Authenticate"));
|
||||
cfg.setAllowCredentials(true);
|
||||
cfg.setMaxAge(3600L);
|
||||
|
||||
@@ -306,6 +306,7 @@ export const usePageEditorExport = ({
|
||||
|
||||
const newStirlingFiles = await actions.addFiles(renamedFiles, {
|
||||
selectFiles: true,
|
||||
skipUploadTracking: true,
|
||||
});
|
||||
if (newStirlingFiles.length > 0) {
|
||||
actions.setSelectedFiles(newStirlingFiles.map((file) => file.fileId));
|
||||
|
||||
@@ -669,7 +669,7 @@ const SignPopout = ({
|
||||
const signedFile = new File([response.data], filename, {
|
||||
type: "application/pdf",
|
||||
});
|
||||
await fileActions.addFiles([signedFile]);
|
||||
await fileActions.addFiles([signedFile], { skipUploadTracking: true });
|
||||
alert({
|
||||
alertType: "success",
|
||||
title: t("success"),
|
||||
@@ -700,7 +700,7 @@ const SignPopout = ({
|
||||
const signedFile = new File([response.data], filename, {
|
||||
type: "application/pdf",
|
||||
});
|
||||
await fileActions.addFiles([signedFile]);
|
||||
await fileActions.addFiles([signedFile], { skipUploadTracking: true });
|
||||
alert({
|
||||
alertType: "success",
|
||||
title: t("success"),
|
||||
|
||||
@@ -258,7 +258,7 @@ const SignRequestWorkbenchView = ({ data }: SignRequestWorkbenchViewProps) => {
|
||||
};
|
||||
|
||||
const handleAddToActiveFiles = async () => {
|
||||
await fileActions.addFiles([pdfFile]);
|
||||
await fileActions.addFiles([pdfFile], { skipUploadTracking: true });
|
||||
alert({
|
||||
alertType: "success",
|
||||
title: t("success"),
|
||||
|
||||
@@ -243,6 +243,7 @@ function FileContextInner({
|
||||
skipAutoUnzip?: boolean;
|
||||
/** Persist to IDB without dispatching to workspace state. */
|
||||
skipWorkspaceDispatch?: boolean;
|
||||
skipUploadTracking?: boolean;
|
||||
},
|
||||
): Promise<StirlingFile[]> => {
|
||||
const stirlingFiles = await addFiles(
|
||||
@@ -286,6 +287,7 @@ function FileContextInner({
|
||||
fileName: string,
|
||||
) => Promise<boolean>;
|
||||
allowDuplicates?: boolean;
|
||||
skipUploadTracking?: boolean;
|
||||
},
|
||||
): Promise<StirlingFile[]> => {
|
||||
const stirlingFiles = await addFiles(
|
||||
|
||||
@@ -20,6 +20,7 @@ import { StirlingFile } from "@app/types/fileContext";
|
||||
import { fileStorage } from "@app/services/fileStorage";
|
||||
import { zipFileService } from "@app/services/zipFileService";
|
||||
import { FileAnalyzer } from "@app/services/fileAnalyzer";
|
||||
import { trackPdfUploaded } from "@app/services/analytics";
|
||||
const DEBUG = process.env.NODE_ENV === "development";
|
||||
const HYDRATION_CONCURRENCY = 2;
|
||||
let activeHydrations = 0;
|
||||
@@ -252,6 +253,7 @@ interface AddFileOptions {
|
||||
fileName: string,
|
||||
) => Promise<boolean>; // Optional callback to confirm extraction of large ZIP files
|
||||
allowDuplicates?: boolean;
|
||||
skipUploadTracking?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -538,6 +540,10 @@ export async function addFiles(
|
||||
);
|
||||
}
|
||||
|
||||
if (!options.skipUploadTracking && stirlingFiles.length > 0) {
|
||||
trackPdfUploaded(stirlingFiles);
|
||||
}
|
||||
|
||||
return stirlingFiles;
|
||||
} finally {
|
||||
// Always release mutex even if error occurs
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
import { createNewStirlingFileStub } from "@app/types/fileContext";
|
||||
import { ToolOperation } from "@app/types/file";
|
||||
import { ensureBackendReady } from "@app/services/backendReadinessGuard";
|
||||
import { trackEditorOperation } from "@app/services/analytics";
|
||||
import { useWillUseCloud } from "@app/hooks/useWillUseCloud";
|
||||
import { useCreditCheck } from "@app/hooks/useCreditCheck";
|
||||
import { notifyPdfProcessingComplete } from "@app/services/desktopNotificationService";
|
||||
@@ -384,6 +385,11 @@ export const useToolOperation = <TParams>(
|
||||
}
|
||||
|
||||
if (processedFiles.length > 0) {
|
||||
trackEditorOperation(
|
||||
config.operationType,
|
||||
successSourceIds.length || validFiles.length,
|
||||
);
|
||||
|
||||
actions.setFiles(processedFiles);
|
||||
|
||||
// Generate thumbnails and download URL concurrently
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
const capture = vi.fn();
|
||||
let optedIn = true;
|
||||
|
||||
vi.mock("posthog-js", () => ({
|
||||
default: {
|
||||
__loaded: true,
|
||||
has_opted_in_capturing: () => optedIn,
|
||||
capture: (...args: unknown[]) => capture(...args),
|
||||
},
|
||||
}));
|
||||
|
||||
import {
|
||||
trackPdfUploaded,
|
||||
trackEditorOperation,
|
||||
} from "@app/services/analytics";
|
||||
|
||||
function pdf(name: string, size = 100): File {
|
||||
return new File([new Uint8Array(size)], name, { type: "application/pdf" });
|
||||
}
|
||||
|
||||
describe("analytics", () => {
|
||||
beforeEach(() => {
|
||||
capture.mockClear();
|
||||
optedIn = true;
|
||||
});
|
||||
|
||||
it("captures one event per uploaded PDF (no dedup)", () => {
|
||||
trackPdfUploaded([pdf("a.pdf"), pdf("a.pdf"), pdf("b.pdf")]);
|
||||
expect(capture).toHaveBeenCalledTimes(3);
|
||||
expect(capture).toHaveBeenCalledWith("editor_pdf_uploaded", {
|
||||
source: "editor",
|
||||
});
|
||||
});
|
||||
|
||||
it("counts every uploaded file regardless of type", () => {
|
||||
trackPdfUploaded([
|
||||
new File(["x"], "a.png", { type: "image/png" }),
|
||||
pdf("b.pdf"),
|
||||
]);
|
||||
expect(capture).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("captures one event per editor operation run", () => {
|
||||
trackEditorOperation("compress", 3);
|
||||
expect(capture).toHaveBeenCalledWith("editor_operation", {
|
||||
source: "editor",
|
||||
tool: "compress",
|
||||
file_count: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not capture when opted out", () => {
|
||||
optedIn = false;
|
||||
trackPdfUploaded([pdf("a.pdf")]);
|
||||
trackEditorOperation("compress", 1);
|
||||
expect(capture).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import posthog from "posthog-js";
|
||||
|
||||
const DEV = process.env.NODE_ENV === "development";
|
||||
|
||||
function canCapture(): boolean {
|
||||
if (typeof window === "undefined") return false;
|
||||
const ph = posthog as unknown as {
|
||||
__loaded?: boolean;
|
||||
has_opted_in_capturing?: () => boolean;
|
||||
};
|
||||
if (!ph.__loaded) return false;
|
||||
return (
|
||||
typeof ph.has_opted_in_capturing !== "function" ||
|
||||
ph.has_opted_in_capturing()
|
||||
);
|
||||
}
|
||||
|
||||
export function trackPdfUploaded(files: File[]): void {
|
||||
try {
|
||||
if (!canCapture() || !files) return;
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
posthog.capture("editor_pdf_uploaded", { source: "editor" });
|
||||
}
|
||||
} catch (error) {
|
||||
if (DEV) console.warn("[analytics] trackPdfUploaded failed", error);
|
||||
}
|
||||
}
|
||||
|
||||
export function trackEditorOperation(toolId: string, fileCount: number): void {
|
||||
try {
|
||||
if (!canCapture()) return;
|
||||
posthog.capture("editor_operation", {
|
||||
source: "editor",
|
||||
tool: toolId,
|
||||
file_count: fileCount,
|
||||
});
|
||||
} catch (error) {
|
||||
if (DEV) console.warn("[analytics] trackEditorOperation failed", error);
|
||||
}
|
||||
}
|
||||
@@ -394,6 +394,7 @@ export async function materializeServerStubs(
|
||||
autoUnzip: boolean;
|
||||
skipAutoUnzip: boolean;
|
||||
allowDuplicates: boolean;
|
||||
skipUploadTracking?: boolean;
|
||||
},
|
||||
) => Promise<StirlingFile[]>;
|
||||
updateStub: (id: FileId, updates: Partial<StirlingFileStub>) => void;
|
||||
@@ -463,6 +464,7 @@ export async function materializeServerStubs(
|
||||
autoUnzip: false,
|
||||
skipAutoUnzip: true,
|
||||
allowDuplicates: true,
|
||||
skipUploadTracking: true,
|
||||
});
|
||||
if (ingested.length === 0) continue;
|
||||
const primary = ingested[ingested.length - 1]!;
|
||||
|
||||
@@ -306,7 +306,11 @@ export interface FileContextActions {
|
||||
// File management - lightweight actions only
|
||||
addFiles: (
|
||||
files: File[],
|
||||
options?: { insertAfterPageId?: string; selectFiles?: boolean },
|
||||
options?: {
|
||||
insertAfterPageId?: string;
|
||||
selectFiles?: boolean;
|
||||
skipUploadTracking?: boolean;
|
||||
},
|
||||
) => Promise<StirlingFile[]>;
|
||||
addFilesWithOptions: (
|
||||
files: File[],
|
||||
@@ -321,6 +325,7 @@ export interface FileContextActions {
|
||||
fileName: string,
|
||||
) => Promise<boolean>;
|
||||
allowDuplicates?: boolean;
|
||||
skipUploadTracking?: boolean;
|
||||
},
|
||||
) => Promise<StirlingFile[]>;
|
||||
addStirlingFileStubs: (
|
||||
|
||||
@@ -299,7 +299,10 @@ export function usePolicyAutoRun(): void {
|
||||
}
|
||||
|
||||
interface ImportContext {
|
||||
addFiles: (files: File[]) => Promise<StirlingFile[]>;
|
||||
addFiles: (
|
||||
files: File[],
|
||||
options?: { skipUploadTracking?: boolean },
|
||||
) => Promise<StirlingFile[]>;
|
||||
consumeFiles: (
|
||||
inputFileIds: FileId[],
|
||||
outputs: StirlingFile[],
|
||||
@@ -462,7 +465,7 @@ async function importOutputs(
|
||||
ctx.bumpRevision();
|
||||
}
|
||||
} else {
|
||||
const added = await ctx.addFiles(files);
|
||||
const added = await ctx.addFiles(files, { skipUploadTracking: true });
|
||||
// Same loop-guard for new-file output: the produced file is a new workspace
|
||||
// file the auto-run would otherwise re-enforce indefinitely.
|
||||
for (const f of added) markDispatched(run.categoryId, f.fileId);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from "axios";
|
||||
import { withBasePath } from "@app/constants/app";
|
||||
import { getBrowserId } from "@app/utils/browserIdentifier";
|
||||
|
||||
let isRefreshing = false;
|
||||
let failedQueue: Array<{
|
||||
@@ -125,6 +126,8 @@ export function setupApiInterceptors(client: AxiosInstance): void {
|
||||
}
|
||||
}
|
||||
|
||||
config.headers["X-Browser-Id"] = getBrowserId();
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
handlePaygError,
|
||||
} from "@app/services/paygErrorInterceptor";
|
||||
import { withBasePath } from "@app/constants/app";
|
||||
import { getBrowserId } from "@app/utils/browserIdentifier";
|
||||
|
||||
// Helper: decode base64url JWT payload safely
|
||||
function decodeJwtPayload(token: string): Record<string, unknown> | null {
|
||||
@@ -82,6 +83,8 @@ apiClient.interceptors.request.use(
|
||||
console.error("[API Client] Error in request interceptor:", error);
|
||||
}
|
||||
|
||||
config.headers["X-Browser-Id"] = getBrowserId();
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
|
||||
Reference in New Issue
Block a user