merge: billing PG session cost fallback
Memind CI / Test, build, and release guards (push) Successful in 17m14s
Memind CI / Test, build, and release guards (push) Successful in 17m14s
This commit is contained in:
+36
-13
@@ -1,4 +1,5 @@
|
|||||||
import { normalizeTokenState } from './billing.mjs';
|
import { normalizeTokenState } from './billing.mjs';
|
||||||
|
import { fetchGooseSessionAccumulatedCostUsd } from './goose-session-cost.mjs';
|
||||||
|
|
||||||
export function loadCostEstimateConfig(env = process.env) {
|
export function loadCostEstimateConfig(env = process.env) {
|
||||||
const useBackendCost = env.H5_USE_BACKEND_COST === '1';
|
const useBackendCost = env.H5_USE_BACKEND_COST === '1';
|
||||||
@@ -56,22 +57,44 @@ export function enrichTokenStateForBilling(
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resolveSessionCostPayload(sessionId, fetchSession, fetchSessionCostFromPg, env) {
|
||||||
|
let sessionCost = null;
|
||||||
|
if (typeof fetchSession === 'function' && sessionId) {
|
||||||
|
try {
|
||||||
|
sessionCost = await fetchSession(sessionId);
|
||||||
|
} catch {
|
||||||
|
sessionCost = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pickSessionAccumulatedCost(sessionCost) != null) {
|
||||||
|
return sessionCost;
|
||||||
|
}
|
||||||
|
|
||||||
|
const readPgCost =
|
||||||
|
typeof fetchSessionCostFromPg === 'function'
|
||||||
|
? fetchSessionCostFromPg
|
||||||
|
: (sid) => fetchGooseSessionAccumulatedCostUsd(sid, env);
|
||||||
|
const pgUsd = env.H5_USE_BACKEND_COST === '1' ? await readPgCost(sessionId) : null;
|
||||||
|
if (pgUsd != null) {
|
||||||
|
return {
|
||||||
|
...(sessionCost && typeof sessionCost === 'object' ? sessionCost : {}),
|
||||||
|
accumulated_cost: pgUsd,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return sessionCost;
|
||||||
|
}
|
||||||
|
|
||||||
export async function resolveBillingTokenState(
|
export async function resolveBillingTokenState(
|
||||||
tokenStateRaw,
|
tokenStateRaw,
|
||||||
{ sessionId = null, fetchSession = null } = {},
|
{ sessionId = null, fetchSession = null, fetchSessionCostFromPg = null } = {},
|
||||||
env = process.env,
|
env = process.env,
|
||||||
) {
|
) {
|
||||||
const state = normalizeTokenState(tokenStateRaw);
|
const state = normalizeTokenState(tokenStateRaw);
|
||||||
|
const sessionCost = await resolveSessionCostPayload(
|
||||||
if (typeof fetchSession === 'function' && sessionId) {
|
sessionId,
|
||||||
try {
|
fetchSession,
|
||||||
const session = await fetchSession(sessionId);
|
fetchSessionCostFromPg,
|
||||||
const enriched = enrichTokenStateForBilling(state, { sessionCost: session }, env);
|
env,
|
||||||
if (enriched.accumulatedCost != null) return enriched;
|
);
|
||||||
} catch {
|
return enrichTokenStateForBilling(state, { sessionCost }, env);
|
||||||
// Best-effort: fall through to inline cost / token estimate.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return enrichTokenStateForBilling(state, {}, env);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,6 +103,23 @@ test('resolveBillingTokenState replaces inflated Finish cost with session cost',
|
|||||||
assert.equal(resolved.accumulatedCost, 0.006738208);
|
assert.equal(resolved.accumulatedCost, 0.006738208);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('resolveBillingTokenState uses PG cost when goosed session API omits accumulated_cost', async () => {
|
||||||
|
const resolved = await resolveBillingTokenState(
|
||||||
|
{
|
||||||
|
accumulatedInputTokens: 280030,
|
||||||
|
accumulatedOutputTokens: 5797,
|
||||||
|
accumulatedCost: 0.082,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sessionId: '20260804_19',
|
||||||
|
fetchSession: async () => ({ id: '20260804_19', accumulated_cost: null }),
|
||||||
|
fetchSessionCostFromPg: async () => 0.006738208,
|
||||||
|
},
|
||||||
|
{ H5_USE_BACKEND_COST: '1' },
|
||||||
|
);
|
||||||
|
assert.equal(resolved.accumulatedCost, 0.006738208);
|
||||||
|
});
|
||||||
|
|
||||||
test('enriched cost drives 1.2x billing instead of flat fallback', () => {
|
test('enriched cost drives 1.2x billing instead of flat fallback', () => {
|
||||||
const previous = { lastInputTokens: 224853, lastOutputTokens: 6685, lastAccumulatedCost: null };
|
const previous = { lastInputTokens: 224853, lastOutputTokens: 6685, lastAccumulatedCost: null };
|
||||||
const tokenState = enrichTokenStateForBilling(
|
const tokenState = enrichTokenStateForBilling(
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
let sharedPgClientPromise = null;
|
||||||
|
|
||||||
|
export function resolveGooseSessionPgUrl(env = process.env) {
|
||||||
|
const explicit = String(env.GOOSE_SESSION_DB_URL ?? '').trim();
|
||||||
|
if (explicit) return explicit;
|
||||||
|
const host = env.GOOSE_SESSION_PG_HOST ?? '127.0.0.1';
|
||||||
|
const port = env.GOOSE_SESSION_PG_PORT ?? '5432';
|
||||||
|
const database = env.GOOSE_SESSION_PG_DATABASE ?? 'memind_sessions';
|
||||||
|
const user = env.GOOSE_SESSION_PG_USER ?? 'john';
|
||||||
|
const password = env.GOOSE_SESSION_PG_PASSWORD ?? '';
|
||||||
|
if (password) {
|
||||||
|
return `postgresql://${encodeURIComponent(user)}:${encodeURIComponent(password)}@${host}:${port}/${database}`;
|
||||||
|
}
|
||||||
|
return `postgresql://${user}@${host}:${port}/${database}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isGooseSessionPgConfigured(env = process.env) {
|
||||||
|
if (String(env.GOOSE_SESSION_DB_URL ?? '').trim()) return true;
|
||||||
|
return String(env.GOOSE_SESSION_PG_DISABLE ?? '') !== '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSharedPgClient(env = process.env) {
|
||||||
|
if (!isGooseSessionPgConfigured(env)) return null;
|
||||||
|
if (!sharedPgClientPromise) {
|
||||||
|
sharedPgClientPromise = import('pg')
|
||||||
|
.then(async ({ default: pg }) => {
|
||||||
|
const client = new pg.Client({ connectionString: resolveGooseSessionPgUrl(env) });
|
||||||
|
await client.connect();
|
||||||
|
return client;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
sharedPgClientPromise = null;
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return sharedPgClientPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGooseSessionAccumulatedCostUsd(sessionId, env = process.env) {
|
||||||
|
const normalizedSessionId = String(sessionId ?? '').trim();
|
||||||
|
if (!normalizedSessionId || !isGooseSessionPgConfigured(env)) return null;
|
||||||
|
try {
|
||||||
|
const client = await getSharedPgClient(env);
|
||||||
|
const result = await client.query(
|
||||||
|
`SELECT accumulated_cost FROM sessions WHERE id = $1 LIMIT 1`,
|
||||||
|
[normalizedSessionId],
|
||||||
|
);
|
||||||
|
const raw = result.rows[0]?.accumulated_cost;
|
||||||
|
if (raw == null) return null;
|
||||||
|
const value = Number(raw);
|
||||||
|
return Number.isFinite(value) && value >= 0 ? value : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createGooseSessionCostReader(env = process.env) {
|
||||||
|
return (sessionId) => fetchGooseSessionAccumulatedCostUsd(sessionId, env);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function closeGooseSessionPgClient() {
|
||||||
|
if (!sharedPgClientPromise) return;
|
||||||
|
try {
|
||||||
|
const client = await sharedPgClientPromise;
|
||||||
|
await client.end();
|
||||||
|
} catch {
|
||||||
|
// ignore shutdown errors in tests/scripts
|
||||||
|
} finally {
|
||||||
|
sharedPgClientPromise = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {
|
||||||
|
isGooseSessionPgConfigured,
|
||||||
|
resolveGooseSessionPgUrl,
|
||||||
|
} from './goose-session-cost.mjs';
|
||||||
|
|
||||||
|
test('resolveGooseSessionPgUrl prefers GOOSE_SESSION_DB_URL', () => {
|
||||||
|
assert.equal(
|
||||||
|
resolveGooseSessionPgUrl({ GOOSE_SESSION_DB_URL: 'postgresql://u:p@host:5432/db' }),
|
||||||
|
'postgresql://u:p@host:5432/db',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolveGooseSessionPgUrl builds local default DSN', () => {
|
||||||
|
assert.equal(
|
||||||
|
resolveGooseSessionPgUrl({
|
||||||
|
GOOSE_SESSION_PG_HOST: '127.0.0.1',
|
||||||
|
GOOSE_SESSION_PG_PORT: '5432',
|
||||||
|
GOOSE_SESSION_PG_DATABASE: 'memind_sessions',
|
||||||
|
GOOSE_SESSION_PG_USER: 'john',
|
||||||
|
}),
|
||||||
|
'postgresql://john@127.0.0.1:5432/memind_sessions',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isGooseSessionPgConfigured can be disabled explicitly', () => {
|
||||||
|
assert.equal(isGooseSessionPgConfigured({ GOOSE_SESSION_PG_DISABLE: '1' }), false);
|
||||||
|
assert.equal(isGooseSessionPgConfigured({ GOOSE_SESSION_DB_URL: 'postgresql://x' }), true);
|
||||||
|
});
|
||||||
+17
-1
@@ -68,6 +68,20 @@ export function resolveAnalyticsOwnerLabel(user = {}) {
|
|||||||
return label.replace(/[\r\n\t]+/g, ' ').slice(0, 80) || '未命名用户';
|
return label.replace(/[\r\n\t]+/g, ' ').slice(0, 80) || '未命名用户';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildViewerAnalyticsIdentity(viewer, config = {}) {
|
||||||
|
if (!viewer?.id) return null;
|
||||||
|
const distinctId = resolveAnalyticsIdentity(viewer.id, config);
|
||||||
|
if (!distinctId) return null;
|
||||||
|
return {
|
||||||
|
distinctId,
|
||||||
|
username: resolveAnalyticsOwnerLabel(viewer),
|
||||||
|
ownerSegment: resolveAnalyticsOwnerSegment(viewer),
|
||||||
|
planType: resolveAnalyticsPlan(viewer),
|
||||||
|
channel: 'public',
|
||||||
|
identityMode: config.identityMode === 'raw' ? 'raw' : 'pseudonymous',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function buildProductAnalyticsContext({ config, user = null } = {}) {
|
export function buildProductAnalyticsContext({ config, user = null } = {}) {
|
||||||
if (!config?.enabled || !config.websiteId) return { enabled: false };
|
if (!config?.enabled || !config.websiteId) return { enabled: false };
|
||||||
const distinctId = user?.id ? resolveAnalyticsIdentity(user.id, config) : '';
|
const distinctId = user?.id ? resolveAnalyticsIdentity(user.id, config) : '';
|
||||||
@@ -152,6 +166,7 @@ export function injectMindSpaceAnalytics(html, {
|
|||||||
planType = 'unknown',
|
planType = 'unknown',
|
||||||
generatedAt = '',
|
generatedAt = '',
|
||||||
channel = 'h5',
|
channel = 'h5',
|
||||||
|
viewerIdentity = null,
|
||||||
config = resolveMindSpaceAnalyticsConfig(),
|
config = resolveMindSpaceAnalyticsConfig(),
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const source = String(html ?? '');
|
const source = String(html ?? '');
|
||||||
@@ -167,7 +182,8 @@ export function injectMindSpaceAnalytics(html, {
|
|||||||
`data-host-url="${config.hostPath}"`,
|
`data-host-url="${config.hostPath}"`,
|
||||||
];
|
];
|
||||||
if (config.domains) attrs.push(`data-domains="${config.domains.replaceAll('"', '"')}"`);
|
if (config.domains) attrs.push(`data-domains="${config.domains.replaceAll('"', '"')}"`);
|
||||||
const block = `<script ${attrs.join(' ')} defer src="${config.scriptPath}"></script><script ${ANALYTICS_MARKER}>(function(){var d=${jsonForInlineScript(metadata)},seen={};function safeTarget(h){if(!h)return'';try{var u=new URL(h,location.href);return u.origin===location.origin?u.pathname:'external:'+u.hostname;}catch{return'';}}function t(n,x){if(!window.umami||typeof window.umami.track!=='function')return;var p=Object.assign({},d,{page_url:location.href,page_route:location.pathname,page_title:document.title},x||{});window.umami.track(n,p);}function identify(){if(!window.umami||typeof window.umami.identify!=='function')return;window.umami.identify(d.owner_id,{username:d.username,memind_page_url:location.href,owner_segment:d.owner_segment,plan_type:d.plan_type,channel:d.channel,surface:d.surface,identity_mode:d.identity_mode});}function pageview(){if(!window.umami||typeof window.umami.track!=='function')return;window.umami.track();}function once(n,x){if(seen[n])return;seen[n]=1;t(n,x);}function ready(){identify();pageview();document.addEventListener('click',function(e){var el=e.target&&e.target.closest?e.target.closest('a,button,[role="button"],[data-umami-event]'):null;if(!el)return;var custom=el.getAttribute('data-umami-event');var href=el.tagName==='A'?el.getAttribute('href')||'':'';t(custom||'page_click',{element:el.tagName.toLowerCase(),element_id:el.id||'',event_label:(custom||el.getAttribute('aria-label')||'').slice(0,100),target_path:safeTarget(href)});},{passive:true});document.addEventListener('submit',function(e){var form=e.target;t('page_form_submit',{form_id:form&&form.id||'',form_action:safeTarget(form&&form.getAttribute('action')||'')});},{passive:true});var marks=[25,50,75,90];function scroll(){var h=document.documentElement.scrollHeight-window.innerHeight;if(h<=0){once('page_scroll_100');return;}var pct=Math.round(window.scrollY/h*100);marks.forEach(function(m){if(pct>=m)once('page_scroll_'+m);});}window.addEventListener('scroll',scroll,{passive:true});setTimeout(function(){once('page_engaged_10s');},10000);setTimeout(function(){once('page_engaged_30s');},30000);scroll();}if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',ready,{once:true});}else{ready();}})();</script>`;
|
const viewerJson = viewerIdentity ? jsonForInlineScript(viewerIdentity) : 'null';
|
||||||
|
const block = `<script ${attrs.join(' ')} defer src="${config.scriptPath}"></script><script ${ANALYTICS_MARKER}>(function(){var d=${jsonForInlineScript(metadata)},v=${viewerJson},seen={};function safeTarget(h){if(!h)return'';try{var u=new URL(h,location.href);return u.origin===location.origin?u.pathname:'external:'+u.hostname;}catch{return'';}}function t(n,x){if(!window.umami||typeof window.umami.track!=='function')return;var p=Object.assign({},d,{page_url:location.href,page_route:location.pathname,page_title:document.title},x||{});window.umami.track(n,p);}function identifyViewer(){if(!v||!v.distinctId||!window.umami||typeof window.umami.identify!=='function')return;window.umami.identify(v.distinctId,{username:v.username||'',memind_page_url:location.href,owner_segment:v.ownerSegment||'',plan_type:v.planType||'',channel:v.channel||'public',surface:'generated_page',identity_mode:v.identityMode||'pseudonymous'});}function pageview(){if(!window.umami||typeof window.umami.track!=='function')return;window.umami.track();}function once(n,x){if(seen[n])return;seen[n]=1;t(n,x);}function ready(){identifyViewer();pageview();document.addEventListener('click',function(e){var el=e.target&&e.target.closest?e.target.closest('a,button,[role="button"],[data-umami-event]'):null;if(!el)return;var custom=el.getAttribute('data-umami-event');var href=el.tagName==='A'?el.getAttribute('href')||'':'';t(custom||'page_click',{element:el.tagName.toLowerCase(),element_id:el.id||'',event_label:(custom||el.getAttribute('aria-label')||'').slice(0,100),target_path:safeTarget(href)});},{passive:true});document.addEventListener('submit',function(e){var form=e.target;t('page_form_submit',{form_id:form&&form.id||'',form_action:safeTarget(form&&form.getAttribute('action')||'')});},{passive:true});var marks=[25,50,75,90];function scroll(){var h=document.documentElement.scrollHeight-window.innerHeight;if(h<=0){once('page_scroll_100');return;}var pct=Math.round(window.scrollY/h*100);marks.forEach(function(m){if(pct>=m)once('page_scroll_'+m);});}window.addEventListener('scroll',scroll,{passive:true});setTimeout(function(){once('page_engaged_10s');},10000);setTimeout(function(){once('page_engaged_30s');},30000);scroll();}if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',ready,{once:true});}else{ready();}})();</script>`;
|
||||||
if (/<\/head>/i.test(source)) return source.replace(/<\/head>/i, `${block}</head>`);
|
if (/<\/head>/i.test(source)) return source.replace(/<\/head>/i, `${block}</head>`);
|
||||||
return source.replace(/<body\b/i, `${block}<body`);
|
return source.replace(/<body\b/i, `${block}<body`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import vm from 'node:vm';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
buildProductAnalyticsContext,
|
buildProductAnalyticsContext,
|
||||||
|
buildViewerAnalyticsIdentity,
|
||||||
injectMindSpaceAnalytics,
|
injectMindSpaceAnalytics,
|
||||||
pseudonymizeAnalyticsId,
|
pseudonymizeAnalyticsId,
|
||||||
resolveAnalyticsIdentity,
|
resolveAnalyticsIdentity,
|
||||||
@@ -124,9 +125,10 @@ test('injects one local same-origin tracker with page dimensions', () => {
|
|||||||
assert.match(out, /src="\/analytics\/script\.js"/);
|
assert.match(out, /src="\/analytics\/script\.js"/);
|
||||||
assert.match(out, /data-host-url="\/analytics"/);
|
assert.match(out, /data-host-url="\/analytics"/);
|
||||||
assert.match(out, /data-auto-track="false"/);
|
assert.match(out, /data-auto-track="false"/);
|
||||||
assert.match(out, /window\.umami\.identify\(d\.owner_id,\{username:d\.username,memind_page_url:location\.href,owner_segment:d\.owner_segment,plan_type:d\.plan_type,channel:d\.channel,surface:d\.surface,identity_mode:d\.identity_mode\}\)/);
|
assert.doesNotMatch(out, /umami\.identify\(d\.owner_id/);
|
||||||
|
assert.match(out, /function identifyViewer\(\)/);
|
||||||
|
assert.match(out, /identifyViewer\(\);pageview\(\)/);
|
||||||
assert.match(out, /function pageview\(\).*window\.umami\.track\(\)/);
|
assert.match(out, /function pageview\(\).*window\.umami\.track\(\)/);
|
||||||
assert.ok(out.indexOf('identify();pageview();') > 0);
|
|
||||||
assert.doesNotMatch(out, /t\('page_view'\)/);
|
assert.doesNotMatch(out, /t\('page_view'\)/);
|
||||||
assert.match(out, /page_id/);
|
assert.match(out, /page_id/);
|
||||||
assert.match(out, /owner_segment/);
|
assert.match(out, /owner_segment/);
|
||||||
@@ -143,7 +145,7 @@ test('injects one local same-origin tracker with page dimensions', () => {
|
|||||||
assert.equal(injectMindSpaceAnalytics(out, { ownerId: 'user-123', config: { enabled: true, websiteId: 'local-website', idSecret: 'secret' } }), out);
|
assert.equal(injectMindSpaceAnalytics(out, { ownerId: 'user-123', config: { enabled: true, websiteId: 'local-website', idSecret: 'secret' } }), out);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('identifies the pseudonymous owner before sending a standard page view', () => {
|
test('public visitors skip creator identify and only send a standard page view', () => {
|
||||||
const out = injectMindSpaceAnalytics('<!doctype html><html><head></head><body></body></html>', {
|
const out = injectMindSpaceAnalytics('<!doctype html><html><head></head><body></body></html>', {
|
||||||
ownerId: 'user-123',
|
ownerId: 'user-123',
|
||||||
ownerSegment: 'plan:pro',
|
ownerSegment: 'plan:pro',
|
||||||
@@ -177,22 +179,67 @@ test('identifies the pseudonymous owner before sending a standard page view', ()
|
|||||||
documentElement: { scrollHeight: 1600 },
|
documentElement: { scrollHeight: 1600 },
|
||||||
addEventListener: () => {},
|
addEventListener: () => {},
|
||||||
},
|
},
|
||||||
location: { href: 'https://m.tkmind.cn/MindSpace/demo/public/page.html' },
|
location: { href: 'https://m.tkmind.cn/MindSpace/demo/public/page.html', pathname: '/MindSpace/demo/public/page.html' },
|
||||||
|
setTimeout: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(JSON.parse(JSON.stringify(calls)), [['track']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('logged-in public visitors identify themselves without using the page creator id', () => {
|
||||||
|
const viewerId = pseudonymizeAnalyticsId('viewer-456', 'secret');
|
||||||
|
const out = injectMindSpaceAnalytics('<!doctype html><html><head></head><body></body></html>', {
|
||||||
|
ownerId: 'user-123',
|
||||||
|
ownerLabel: '张三',
|
||||||
|
viewerIdentity: buildViewerAnalyticsIdentity(
|
||||||
|
{ id: 'viewer-456', displayName: '李四', role: 'user', planType: 'free' },
|
||||||
|
{ idSecret: 'secret', identityMode: 'pseudonymous' },
|
||||||
|
),
|
||||||
|
config: {
|
||||||
|
enabled: true,
|
||||||
|
websiteId: 'local-website',
|
||||||
|
idSecret: 'secret',
|
||||||
|
scriptPath: '/analytics/script.js',
|
||||||
|
hostPath: '/analytics',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const inlineScript = out.match(/<script data-memind-analytics="1">([\s\S]*?)<\/script>/)?.[1];
|
||||||
|
assert.ok(inlineScript);
|
||||||
|
|
||||||
|
const calls = [];
|
||||||
|
vm.runInNewContext(inlineScript, {
|
||||||
|
window: {
|
||||||
|
umami: {
|
||||||
|
identify: (...args) => calls.push(['identify', ...args]),
|
||||||
|
track: (...args) => calls.push(['track', ...args]),
|
||||||
|
},
|
||||||
|
innerHeight: 800,
|
||||||
|
scrollY: 0,
|
||||||
|
addEventListener: () => {},
|
||||||
|
},
|
||||||
|
document: {
|
||||||
|
readyState: 'complete',
|
||||||
|
title: 'Demo',
|
||||||
|
documentElement: { scrollHeight: 1600 },
|
||||||
|
addEventListener: () => {},
|
||||||
|
},
|
||||||
|
location: { href: 'https://m.tkmind.cn/MindSpace/demo/public/page.html', pathname: '/MindSpace/demo/public/page.html' },
|
||||||
setTimeout: () => {},
|
setTimeout: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepEqual(JSON.parse(JSON.stringify(calls)), [
|
assert.deepEqual(JSON.parse(JSON.stringify(calls)), [
|
||||||
['identify', pseudonymizeAnalyticsId('user-123', 'secret'), {
|
['identify', viewerId, {
|
||||||
username: '张三',
|
username: '李四',
|
||||||
memind_page_url: 'https://m.tkmind.cn/MindSpace/demo/public/page.html',
|
memind_page_url: 'https://m.tkmind.cn/MindSpace/demo/public/page.html',
|
||||||
owner_segment: 'plan:pro',
|
owner_segment: 'plan:free',
|
||||||
channel: 'h5',
|
plan_type: 'free',
|
||||||
|
channel: 'public',
|
||||||
surface: 'generated_page',
|
surface: 'generated_page',
|
||||||
plan_type: 'unknown',
|
|
||||||
identity_mode: 'pseudonymous',
|
identity_mode: 'pseudonymous',
|
||||||
}],
|
}],
|
||||||
['track'],
|
['track'],
|
||||||
]);
|
]);
|
||||||
|
assert.notEqual(viewerId, 'user-123');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('does not alter non-full-html or disabled pages', () => {
|
test('does not alter non-full-html or disabled pages', () => {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import path from 'node:path';
|
|||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import mysql from 'mysql2/promise';
|
import mysql from 'mysql2/promise';
|
||||||
import pg from 'pg';
|
import pg from 'pg';
|
||||||
|
import { resolveGooseSessionPgUrl } from '../goose-session-cost.mjs';
|
||||||
|
|
||||||
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
|
||||||
@@ -33,9 +34,14 @@ loadEnvFile(path.join(root, '.env'));
|
|||||||
|
|
||||||
const apply = process.argv.includes('--apply');
|
const apply = process.argv.includes('--apply');
|
||||||
const sinceArg = process.argv.find((a) => a.startsWith('--since='));
|
const sinceArg = process.argv.find((a) => a.startsWith('--since='));
|
||||||
const sinceDate = sinceArg ? sinceArg.slice('--since='.length) : '2026-08-04';
|
const sinceRaw = sinceArg ? sinceArg.slice('--since='.length) : '2026-08-04';
|
||||||
const startMs = new Date(`${sinceDate}T00:00:00+08:00`).getTime();
|
const startMs = sinceRaw.includes('T')
|
||||||
const DEDupe_NOTE_PREFIX = `补偿:Token估价超扣(${sinceDate}起)`;
|
? new Date(sinceRaw).getTime()
|
||||||
|
: new Date(`${sinceRaw}T00:00:00+08:00`).getTime();
|
||||||
|
const sinceLabel = sinceRaw.includes('T')
|
||||||
|
? sinceRaw.replace('T', ' ').replace('+08:00', ' CST')
|
||||||
|
: `${sinceRaw} 00:00 CST`;
|
||||||
|
const DEDupe_NOTE_PREFIX = `补偿:Token估价超扣(${sinceLabel}起)`;
|
||||||
|
|
||||||
function loadBillingConfig() {
|
function loadBillingConfig() {
|
||||||
const marginMultiplier = Number(process.env.H5_MARGIN_MULTIPLIER ?? 1);
|
const marginMultiplier = Number(process.env.H5_MARGIN_MULTIPLIER ?? 1);
|
||||||
@@ -56,12 +62,7 @@ function correctTotalCents(gooseCostUsd, config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolvePgUrl() {
|
function resolvePgUrl() {
|
||||||
if (process.env.GOOSE_SESSION_DB_URL) return process.env.GOOSE_SESSION_DB_URL;
|
return resolveGooseSessionPgUrl(process.env);
|
||||||
const host = process.env.GOOSE_SESSION_PG_HOST ?? '127.0.0.1';
|
|
||||||
const port = process.env.GOOSE_SESSION_PG_PORT ?? '5432';
|
|
||||||
const db = process.env.GOOSE_SESSION_PG_DATABASE ?? 'memind_sessions';
|
|
||||||
const user = process.env.GOOSE_SESSION_PG_USER ?? 'john';
|
|
||||||
return `postgresql://${user}@${host}:${port}/${db}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
@@ -102,7 +103,7 @@ async function main() {
|
|||||||
|
|
||||||
const sessionIds = [...bySession.keys()];
|
const sessionIds = [...bySession.keys()];
|
||||||
if (sessionIds.length === 0) {
|
if (sessionIds.length === 0) {
|
||||||
console.log('No usage records since', sinceDate);
|
console.log('No usage records since', sinceLabel);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +151,7 @@ async function main() {
|
|||||||
const totalRefund = users.reduce((sum, user) => sum + user.refundCents, 0);
|
const totalRefund = users.reduce((sum, user) => sum + user.refundCents, 0);
|
||||||
|
|
||||||
console.log('Billing config:', config);
|
console.log('Billing config:', config);
|
||||||
console.log('Since:', sinceDate, `(${startMs})`);
|
console.log('Since:', sinceLabel, `(${startMs})`);
|
||||||
console.log('Affected users:', users.length);
|
console.log('Affected users:', users.length);
|
||||||
console.log('Total refund:', `¥${(totalRefund / 100).toFixed(2)}`, `(${totalRefund} cents)`);
|
console.log('Total refund:', `¥${(totalRefund / 100).toFixed(2)}`, `(${totalRefund} cents)`);
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
collectInlineScriptHashes,
|
collectInlineScriptHashes,
|
||||||
} from '../mindspace-public-delivery.mjs';
|
} from '../mindspace-public-delivery.mjs';
|
||||||
import {
|
import {
|
||||||
|
buildViewerAnalyticsIdentity,
|
||||||
injectMindSpaceAnalytics,
|
injectMindSpaceAnalytics,
|
||||||
resolveAnalyticsOwnerLabel,
|
resolveAnalyticsOwnerLabel,
|
||||||
resolveAnalyticsOwnerSegment,
|
resolveAnalyticsOwnerSegment,
|
||||||
@@ -51,6 +52,7 @@ async function decoratePublicationHtmlAnalytics(
|
|||||||
result,
|
result,
|
||||||
analyticsConfig,
|
analyticsConfig,
|
||||||
rybbitConfig,
|
rybbitConfig,
|
||||||
|
viewer = null,
|
||||||
getAuthPool = () => null,
|
getAuthPool = () => null,
|
||||||
getUserAuth = () => null,
|
getUserAuth = () => null,
|
||||||
getMindSpacePages = () => null,
|
getMindSpacePages = () => null,
|
||||||
@@ -112,6 +114,7 @@ async function decoratePublicationHtmlAnalytics(
|
|||||||
generatedAt: pageDataContext?.generatedAt ?? '',
|
generatedAt: pageDataContext?.generatedAt ?? '',
|
||||||
pageId,
|
pageId,
|
||||||
publicationId,
|
publicationId,
|
||||||
|
viewerIdentity: buildViewerAnalyticsIdentity(viewer, analyticsConfig),
|
||||||
config: analyticsConfig,
|
config: analyticsConfig,
|
||||||
});
|
});
|
||||||
decorated = injectMindSpaceRybbit(decorated, {
|
decorated = injectMindSpaceRybbit(decorated, {
|
||||||
@@ -215,6 +218,7 @@ export function createPortalPublishedPageDelivery({
|
|||||||
result,
|
result,
|
||||||
analyticsConfig,
|
analyticsConfig,
|
||||||
rybbitConfig,
|
rybbitConfig,
|
||||||
|
viewer: req.currentUser ?? null,
|
||||||
getAuthPool,
|
getAuthPool,
|
||||||
getUserAuth,
|
getUserAuth,
|
||||||
getMindSpacePages,
|
getMindSpacePages,
|
||||||
@@ -346,6 +350,7 @@ export function createPortalPublishedPageDelivery({
|
|||||||
result,
|
result,
|
||||||
analyticsConfig,
|
analyticsConfig,
|
||||||
rybbitConfig,
|
rybbitConfig,
|
||||||
|
viewer: req.currentUser ?? null,
|
||||||
getAuthPool,
|
getAuthPool,
|
||||||
getUserAuth,
|
getUserAuth,
|
||||||
getMindSpacePages,
|
getMindSpacePages,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
buildViewerAnalyticsIdentity,
|
||||||
injectMindSpaceAnalytics,
|
injectMindSpaceAnalytics,
|
||||||
resolveAnalyticsOwnerLabel,
|
resolveAnalyticsOwnerLabel,
|
||||||
resolveAnalyticsOwnerSegment,
|
resolveAnalyticsOwnerSegment,
|
||||||
@@ -250,6 +251,10 @@ export function createPortalWorkspacePublicationDelivery({
|
|||||||
pageDataContext?.publicationId ??
|
pageDataContext?.publicationId ??
|
||||||
pageDataContext?.publication_id ??
|
pageDataContext?.publication_id ??
|
||||||
'',
|
'',
|
||||||
|
viewerIdentity: buildViewerAnalyticsIdentity(
|
||||||
|
req.currentUser ?? null,
|
||||||
|
analyticsConfig,
|
||||||
|
),
|
||||||
config: analyticsConfig,
|
config: analyticsConfig,
|
||||||
});
|
});
|
||||||
html = injectMindSpaceRybbit(html, {
|
html = injectMindSpaceRybbit(html, {
|
||||||
|
|||||||
Reference in New Issue
Block a user