@koendhoore/directus-extension-umami-analytics 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.js +1 -1
- package/package.json +1 -1
- package/src/umami-analytics-endpoint/index.ts +27 -7
package/dist/api.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"node:crypto";const e=new Map;let r=null;function n(t,e){const r=t[e];if(!r)throw new Error(`Missing ${e}`);return r.replace(/\/+$/,"")}function a(t){return n(t,"UMAMI_URL").replace(/\/api\/?$/,"")}function s(t,e){const r=e.replace(/^\/api\/?/,"").replace(/^\/+/,"");return`${a(t)}/api/${r}`}function o(t,e){const r=Number(t);return Number.isFinite(r)?r:e}function i(t){return Buffer.from(t).toString("base64").replace(/=/g,"").replace(/\+/g,"-").replace(/\//g,"_")}function c(e,r){return function(e,r){const n=t.randomBytes(16),a=t.randomBytes(64),s=t.pbkdf2Sync(r,a,1e4,32,"sha512"),o=t.createCipheriv("aes-256-gcm",s,n),i=Buffer.concat([o.update(String(e),"utf8"),o.final()]),c=o.getAuthTag();return Buffer.concat([a,n,c,i]).toString("base64")}(function(e,r){const n=`${i(JSON.stringify({alg:"HS256",typ:"JWT"}))}.${i(JSON.stringify(e))}`;return`${n}.${i(t.createHmac("sha256",r).update(n).digest())}`}(e,r),r)}function u(e){const r=e.UMAMI_USER_ID||e.UMAMI_API_CLIENT_USER_ID,n=e.UMAMI_APP_SECRET||e.UMAMI_API_CLIENT_SECRET;if(!r||!n)return null;return{headers:{Authorization:`Bearer ${c({userId:r},function(...e){return t.createHash("sha512").update(e.join("")).digest("hex")}(n))}`}}}async function
|
|
1
|
+
import t from"node:crypto";const e=new Map;let r=null;function n(t,e){const r=t[e];if(!r)throw new Error(`Missing ${e}`);return r.replace(/\/+$/,"")}function a(t){return n(t,"UMAMI_URL").replace(/\/api\/?$/,"")}function s(t,e){const r=e.replace(/^\/api\/?/,"").replace(/^\/+/,"");return`${a(t)}/api/${r}`}function o(t,e){const r=Number(t);return Number.isFinite(r)?r:e}function i(t){return Buffer.from(t).toString("base64").replace(/=/g,"").replace(/\+/g,"-").replace(/\//g,"_")}function c(e,r){return function(e,r){const n=t.randomBytes(16),a=t.randomBytes(64),s=t.pbkdf2Sync(r,a,1e4,32,"sha512"),o=t.createCipheriv("aes-256-gcm",s,n),i=Buffer.concat([o.update(String(e),"utf8"),o.final()]),c=o.getAuthTag();return Buffer.concat([a,n,c,i]).toString("base64")}(function(e,r){const n=`${i(JSON.stringify({alg:"HS256",typ:"JWT"}))}.${i(JSON.stringify(e))}`;return`${n}.${i(t.createHmac("sha256",r).update(n).digest())}`}(e,r),r)}function u(e){const r=e.UMAMI_USER_ID||e.UMAMI_API_CLIENT_USER_ID,n=e.UMAMI_APP_SECRET||e.UMAMI_API_CLIENT_SECRET;if(!r||!n)return null;return{headers:{Authorization:`Bearer ${c({userId:r},function(...e){return t.createHash("sha512").update(e.join("")).digest("hex")}(n))}`}}}async function f(t){const e=t.UMAMI_API_KEY;if(e)return{headers:{"x-umami-api-key":e}};const a=t.UMAMI_TOKEN;if(a)return{headers:{Authorization:`Bearer ${a}`}};const o=u(t);return o||async function(t){if(r&&r.expires>Date.now())return r.auth;const e=n(t,"UMAMI_USERNAME"),a=n(t,"UMAMI_PASSWORD"),o=await fetch(s(t,"/auth/login"),{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify({username:e,password:a})}),i=await o.text();if(!o.ok)throw new Error(`Umami login failed: ${o.status} ${i}`);let c=null;try{c=i?JSON.parse(i):null}catch{}const u=c?.token||c?.accessToken||c?.data?.token||c?.data?.accessToken,f=o.headers.get("set-cookie")||"",d=u?{headers:{Authorization:`Bearer ${u}`}}:{headers:f?{Cookie:f.split(";")[0]}:{}};return r={expires:Date.now()+33e5,auth:d},d}(t)}async function d(t,e){const r=await f(t),n=await fetch(s(t,e),{headers:{Accept:"application/json",...r.headers}}),a=await n.text();if(!n.ok)throw new Error(`Umami request failed: ${n.status} ${a}`);return a?JSON.parse(a):null}function h(t,e,r,n,a=10){return`/websites/${t}/metrics?${new URLSearchParams({startAt:String(Math.round(r)),endAt:String(Math.round(n)),type:e,limit:String(a)}).toString()}`}async function p(t,e,r=[]){try{return await d(t,e)}catch(t){return{error:t.message||"Unknown Umami error",path:e,fallback:r}}}const m=[],l=[{name:"umami-analytics",config:(t,r)=>{const n=r.env||process.env;t.get("/health",(t,e)=>{e.json({ok:!0,service:"umami-analytics"})}),t.get("/debug",async(t,e)=>{try{const t=n.UMAMI_API_KEY?"api-key":n.UMAMI_TOKEN?"bearer-token":n.UMAMI_USER_ID||n.UMAMI_API_CLIENT_USER_ID?"api-client":"username-password",r=await d(n,"/me").catch(t=>({error:t.message}));e.json({ok:!0,authMode:t,umamiUrl:a(n),websiteId:n.UMAMI_WEBSITE_ID,me:r})}catch(t){e.status(500).json({error:t.message||"Unknown error"})}}),t.get("/summary",async(t,r)=>{try{const a=String(t.query.websiteId||n.UMAMI_WEBSITE_ID||"");if(!a)return r.status(400).json({error:"Missing websiteId or UMAMI_WEBSITE_ID"});const{startAt:s,endAt:i,days:c}=function(t){const e=Date.now(),r=Math.max(1,Math.min(365,o(t.days,30))),n=o(t.endAt,e);return{startAt:o(t.startAt,n-24*r*60*60*1e3),endAt:n,days:r}}(t.query),u=`summary:${a}:${s}:${i}:v3`,f=function(t){const r=e.get(t);return!r||r.expires<Date.now()?null:r.value}(u);if(f)return r.json(f);const m=new URLSearchParams({startAt:String(Math.round(s)),endAt:String(Math.round(i))}),l=await d(n,`/websites/${a}/stats?${m.toString()}`),[M,A,g]=await Promise.all([p(n,h(a,"path",s,i,10),[]),p(n,h(a,"referrer",s,i,10),[]),p(n,h(a,"event",s,i,10),[])]),I={websiteId:a,range:{startAt:s,endAt:i,days:c},stats:l,pages:M,referrers:A,events:g};!function(t,r,n=3e5){e.set(t,{expires:Date.now()+n,value:r})}(u,I),r.json(I)}catch(t){r.status(500).json({error:t.message||"Unknown error"})}})}}],M=[];export{l as endpoints,m as hooks,M as operations};
|
package/package.json
CHANGED
|
@@ -170,6 +170,18 @@ function metricPath(websiteId: string, type: string, startAt: number, endAt: num
|
|
|
170
170
|
return `/websites/${websiteId}/metrics?${params.toString()}`;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
async function safeUmamiGet(env: Env, path: string, fallback: unknown = []) {
|
|
174
|
+
try {
|
|
175
|
+
return await umamiGet(env, path);
|
|
176
|
+
} catch (error: any) {
|
|
177
|
+
return {
|
|
178
|
+
error: error.message || 'Unknown Umami error',
|
|
179
|
+
path,
|
|
180
|
+
fallback,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
173
185
|
export default (router: any, context: any) => {
|
|
174
186
|
const env: Env = context.env || process.env;
|
|
175
187
|
|
|
@@ -207,16 +219,24 @@ export default (router: any, context: any) => {
|
|
|
207
219
|
if (!websiteId) return res.status(400).json({ error: 'Missing websiteId or UMAMI_WEBSITE_ID' });
|
|
208
220
|
|
|
209
221
|
const { startAt, endAt, days } = getRange(req.query);
|
|
210
|
-
const key = `summary:${websiteId}:${startAt}:${endAt}`;
|
|
222
|
+
const key = `summary:${websiteId}:${startAt}:${endAt}:v3`;
|
|
211
223
|
const hit = cached(key);
|
|
212
224
|
if (hit) return res.json(hit);
|
|
213
225
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
226
|
+
// Umami v3:
|
|
227
|
+
// - /stats accepts startAt/endAt only.
|
|
228
|
+
// - /metrics uses type=path, not type=url.
|
|
229
|
+
const statsParams = new URLSearchParams({
|
|
230
|
+
startAt: String(Math.round(startAt)),
|
|
231
|
+
endAt: String(Math.round(endAt)),
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const stats = await umamiGet(env, `/websites/${websiteId}/stats?${statsParams.toString()}`);
|
|
235
|
+
|
|
236
|
+
const [pages, referrers, events] = await Promise.all([
|
|
237
|
+
safeUmamiGet(env, metricPath(websiteId, 'path', startAt, endAt, 10), []),
|
|
238
|
+
safeUmamiGet(env, metricPath(websiteId, 'referrer', startAt, endAt, 10), []),
|
|
239
|
+
safeUmamiGet(env, metricPath(websiteId, 'event', startAt, endAt, 10), []),
|
|
220
240
|
]);
|
|
221
241
|
|
|
222
242
|
const payload = { websiteId, range: { startAt, endAt, days }, stats, pages, referrers, events };
|