visit.ts 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. import { isAfter, subDays, set } from "date-fns";
  2. import * as utils from "../utils";
  3. import redisClient, * as redis from "../redis";
  4. import knex from "../knex";
  5. interface Add {
  6. browser: string;
  7. country: string;
  8. domain?: string;
  9. id: number;
  10. os: string;
  11. referrer: string;
  12. }
  13. export const add = async (params: Add) => {
  14. const data = {
  15. ...params,
  16. country: params.country.toLowerCase(),
  17. referrer: params.referrer.toLowerCase()
  18. };
  19. const visit = await knex<Visit>("visits")
  20. .where({ link_id: params.id })
  21. .andWhere(
  22. knex.raw("date_trunc('hour', created_at) = date_trunc('hour', ?)", [
  23. knex.fn.now()
  24. ])
  25. )
  26. .first();
  27. if (visit) {
  28. await knex("visits")
  29. .where({ id: visit.id })
  30. .increment(`br_${data.browser}`, 1)
  31. .increment(`os_${data.os}`, 1)
  32. .increment("total", 1)
  33. .update({
  34. updated_at: new Date().toISOString(),
  35. countries: knex.raw(
  36. "jsonb_set(countries, '{??}', (COALESCE(countries->>?,'0')::int + 1)::text::jsonb)",
  37. [data.country, data.country]
  38. ),
  39. referrers: knex.raw(
  40. "jsonb_set(referrers, '{??}', (COALESCE(referrers->>?,'0')::int + 1)::text::jsonb)",
  41. [data.referrer, data.referrer]
  42. )
  43. });
  44. } else {
  45. await knex<Visit>("visits").insert({
  46. [`br_${data.browser}`]: 1,
  47. countries: { [data.country]: 1 },
  48. referrers: { [data.referrer]: 1 },
  49. [`os_${data.os}`]: 1,
  50. total: 1,
  51. link_id: data.id
  52. });
  53. }
  54. return visit;
  55. };
  56. interface StatsResult {
  57. stats: {
  58. browser: { name: string; value: number }[];
  59. os: { name: string; value: number }[];
  60. country: { name: string; value: number }[];
  61. referrer: { name: string; value: number }[];
  62. };
  63. views: number[];
  64. }
  65. interface IGetStatsResponse {
  66. allTime: StatsResult;
  67. lastDay: StatsResult;
  68. lastMonth: StatsResult;
  69. lastWeek: StatsResult;
  70. updatedAt: string;
  71. }
  72. export const find = async (match: Partial<Visit>, total: number) => {
  73. if (match.link_id) {
  74. const key = redis.key.stats(match.link_id);
  75. const cached = await redisClient.get(key);
  76. if (cached) return JSON.parse(cached);
  77. }
  78. const stats = {
  79. lastDay: {
  80. stats: utils.getInitStats(),
  81. views: new Array(24).fill(0)
  82. },
  83. lastWeek: {
  84. stats: utils.getInitStats(),
  85. views: new Array(7).fill(0)
  86. },
  87. lastMonth: {
  88. stats: utils.getInitStats(),
  89. views: new Array(30).fill(0)
  90. },
  91. allTime: {
  92. stats: utils.getInitStats(),
  93. views: new Array(18).fill(0)
  94. }
  95. };
  96. const visitsStream: any = knex<Visit>("visits").where(match).stream();
  97. const nowUTC = utils.getUTCDate();
  98. const now = new Date();
  99. for await (const visit of visitsStream as Visit[]) {
  100. utils.STATS_PERIODS.forEach(([days, type]) => {
  101. const isIncluded = isAfter(
  102. new Date(visit.created_at),
  103. subDays(nowUTC, days)
  104. );
  105. if (isIncluded) {
  106. const diffFunction = utils.getDifferenceFunction(type);
  107. const diff = diffFunction(now, new Date(visit.created_at));
  108. const index = stats[type].views.length - diff - 1;
  109. const view = stats[type].views[index];
  110. const period = stats[type].stats;
  111. stats[type].stats = {
  112. browser: {
  113. chrome: period.browser.chrome + visit.br_chrome,
  114. edge: period.browser.edge + visit.br_edge,
  115. firefox: period.browser.firefox + visit.br_firefox,
  116. ie: period.browser.ie + visit.br_ie,
  117. opera: period.browser.opera + visit.br_opera,
  118. other: period.browser.other + visit.br_other,
  119. safari: period.browser.safari + visit.br_safari
  120. },
  121. os: {
  122. android: period.os.android + visit.os_android,
  123. ios: period.os.ios + visit.os_ios,
  124. linux: period.os.linux + visit.os_linux,
  125. macos: period.os.macos + visit.os_macos,
  126. other: period.os.other + visit.os_other,
  127. windows: period.os.windows + visit.os_windows
  128. },
  129. country: {
  130. ...period.country,
  131. ...Object.entries(visit.countries).reduce(
  132. (obj, [country, count]) => ({
  133. ...obj,
  134. [country]: (period.country[country] || 0) + count
  135. }),
  136. {}
  137. )
  138. },
  139. referrer: {
  140. ...period.referrer,
  141. ...Object.entries(visit.referrers).reduce(
  142. (obj, [referrer, count]) => ({
  143. ...obj,
  144. [referrer]: (period.referrer[referrer] || 0) + count
  145. }),
  146. {}
  147. )
  148. }
  149. };
  150. stats[type].views[index] = view + visit.total;
  151. }
  152. });
  153. const allTime = stats.allTime.stats;
  154. const diffFunction = utils.getDifferenceFunction("allTime");
  155. const diff = diffFunction(
  156. set(new Date(), { date: 1 }),
  157. set(new Date(visit.created_at), { date: 1 })
  158. );
  159. const index = stats.allTime.views.length - diff - 1;
  160. const view = stats.allTime.views[index];
  161. stats.allTime.stats = {
  162. browser: {
  163. chrome: allTime.browser.chrome + visit.br_chrome,
  164. edge: allTime.browser.edge + visit.br_edge,
  165. firefox: allTime.browser.firefox + visit.br_firefox,
  166. ie: allTime.browser.ie + visit.br_ie,
  167. opera: allTime.browser.opera + visit.br_opera,
  168. other: allTime.browser.other + visit.br_other,
  169. safari: allTime.browser.safari + visit.br_safari
  170. },
  171. os: {
  172. android: allTime.os.android + visit.os_android,
  173. ios: allTime.os.ios + visit.os_ios,
  174. linux: allTime.os.linux + visit.os_linux,
  175. macos: allTime.os.macos + visit.os_macos,
  176. other: allTime.os.other + visit.os_other,
  177. windows: allTime.os.windows + visit.os_windows
  178. },
  179. country: {
  180. ...allTime.country,
  181. ...Object.entries(visit.countries).reduce(
  182. (obj, [country, count]) => ({
  183. ...obj,
  184. [country]: (allTime.country[country] || 0) + count
  185. }),
  186. {}
  187. )
  188. },
  189. referrer: {
  190. ...allTime.referrer,
  191. ...Object.entries(visit.referrers).reduce(
  192. (obj, [referrer, count]) => ({
  193. ...obj,
  194. [referrer]: (allTime.referrer[referrer] || 0) + count
  195. }),
  196. {}
  197. )
  198. }
  199. };
  200. stats.allTime.views[index] = view + visit.total;
  201. }
  202. const response: IGetStatsResponse = {
  203. allTime: {
  204. stats: utils.statsObjectToArray(stats.allTime.stats),
  205. views: stats.allTime.views
  206. },
  207. lastDay: {
  208. stats: utils.statsObjectToArray(stats.lastDay.stats),
  209. views: stats.lastDay.views
  210. },
  211. lastMonth: {
  212. stats: utils.statsObjectToArray(stats.lastMonth.stats),
  213. views: stats.lastMonth.views
  214. },
  215. lastWeek: {
  216. stats: utils.statsObjectToArray(stats.lastWeek.stats),
  217. views: stats.lastWeek.views
  218. },
  219. updatedAt: new Date().toISOString()
  220. };
  221. if (match.link_id) {
  222. const cacheTime = utils.getStatsCacheTime(total);
  223. const key = redis.key.stats(match.link_id);
  224. redisClient.set(key, JSON.stringify(response), "EX", cacheTime);
  225. }
  226. return response;
  227. };