@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 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 d(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,d=o.headers.get("set-cookie")||"",f=u?{headers:{Authorization:`Bearer ${u}`}}:{headers:d?{Cookie:d.split(";")[0]}:{}};return r={expires:Date.now()+33e5,auth:f},f}(t)}async function f(t,e){const r=await d(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()}`}const p=[],M=[{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 f(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}`,d=function(t){const r=e.get(t);return!r||r.expires<Date.now()?null:r.value}(u);if(d)return r.json(d);const p=new URLSearchParams({startAt:String(Math.round(s)),endAt:String(Math.round(i))}),[M,l,A,m]=await Promise.all([f(n,`/websites/${a}/stats?${p.toString()}`),f(n,h(a,"url",s,i,10)),f(n,h(a,"referrer",s,i,10)),f(n,h(a,"event",s,i,10)).catch(()=>[])]),g={websiteId:a,range:{startAt:s,endAt:i,days:c},stats:M,pages:l,referrers:A,events:m};!function(t,r,n=3e5){e.set(t,{expires:Date.now()+n,value:r})}(u,g),r.json(g)}catch(t){r.status(500).json({error:t.message||"Unknown error"})}})}}],l=[];export{M as endpoints,p as hooks,l as operations};
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koendhoore/directus-extension-umami-analytics",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Directus bundle: secure Umami proxy endpoint + native Insights analytics panel.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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
- const params = new URLSearchParams({ startAt: String(Math.round(startAt)), endAt: String(Math.round(endAt)) });
215
- const [stats, pages, referrers, events] = await Promise.all([
216
- umamiGet(env, `/websites/${websiteId}/stats?${params.toString()}`),
217
- umamiGet(env, metricPath(websiteId, 'url', startAt, endAt, 10)),
218
- umamiGet(env, metricPath(websiteId, 'referrer', startAt, endAt, 10)),
219
- umamiGet(env, metricPath(websiteId, 'event', startAt, endAt, 10)).catch(() => []),
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 };