visit.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. import { isAfter, subDays, set } from "date-fns";
  2. import * as utils from "../utils";
  3. import * 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 redis.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")
  97. .where(match)
  98. .stream();
  99. const nowUTC = utils.getUTCDate();
  100. const now = new Date();
  101. for await (const visit of visitsStream as Visit[]) {
  102. utils.STATS_PERIODS.forEach(([days, type]) => {
  103. const isIncluded = isAfter(
  104. new Date(visit.created_at),
  105. subDays(nowUTC, days)
  106. );
  107. if (isIncluded) {
  108. const diffFunction = utils.getDifferenceFunction(type);
  109. const diff = diffFunction(now, visit.created_at);
  110. const index = stats[type].views.length - diff - 1;
  111. const view = stats[type].views[index];
  112. const period = stats[type].stats;
  113. stats[type].stats = {
  114. browser: {
  115. chrome: period.browser.chrome + visit.br_chrome,
  116. edge: period.browser.edge + visit.br_edge,
  117. firefox: period.browser.firefox + visit.br_firefox,
  118. ie: period.browser.ie + visit.br_ie,
  119. opera: period.browser.opera + visit.br_opera,
  120. other: period.browser.other + visit.br_other,
  121. safari: period.browser.safari + visit.br_safari
  122. },
  123. os: {
  124. android: period.os.android + visit.os_android,
  125. ios: period.os.ios + visit.os_ios,
  126. linux: period.os.linux + visit.os_linux,
  127. macos: period.os.macos + visit.os_macos,
  128. other: period.os.other + visit.os_other,
  129. windows: period.os.windows + visit.os_windows
  130. },
  131. country: {
  132. ...period.country,
  133. ...Object.entries(visit.countries).reduce(
  134. (obj, [country, count]) => ({
  135. ...obj,
  136. [country]: (period.country[country] || 0) + count
  137. }),
  138. {}
  139. )
  140. },
  141. referrer: {
  142. ...period.referrer,
  143. ...Object.entries(visit.referrers).reduce(
  144. (obj, [referrer, count]) => ({
  145. ...obj,
  146. [referrer]: (period.referrer[referrer] || 0) + count
  147. }),
  148. {}
  149. )
  150. }
  151. };
  152. stats[type].views[index] = view + visit.total;
  153. }
  154. });
  155. const allTime = stats.allTime.stats;
  156. const diffFunction = utils.getDifferenceFunction("allTime");
  157. const diff = diffFunction(
  158. set(new Date(), { date: 1 }),
  159. set(new Date(visit.created_at), { date: 1 })
  160. );
  161. const index = stats.allTime.views.length - diff - 1;
  162. const view = stats.allTime.views[index];
  163. stats.allTime.stats = {
  164. browser: {
  165. chrome: allTime.browser.chrome + visit.br_chrome,
  166. edge: allTime.browser.edge + visit.br_edge,
  167. firefox: allTime.browser.firefox + visit.br_firefox,
  168. ie: allTime.browser.ie + visit.br_ie,
  169. opera: allTime.browser.opera + visit.br_opera,
  170. other: allTime.browser.other + visit.br_other,
  171. safari: allTime.browser.safari + visit.br_safari
  172. },
  173. os: {
  174. android: allTime.os.android + visit.os_android,
  175. ios: allTime.os.ios + visit.os_ios,
  176. linux: allTime.os.linux + visit.os_linux,
  177. macos: allTime.os.macos + visit.os_macos,
  178. other: allTime.os.other + visit.os_other,
  179. windows: allTime.os.windows + visit.os_windows
  180. },
  181. country: {
  182. ...allTime.country,
  183. ...Object.entries(visit.countries).reduce(
  184. (obj, [country, count]) => ({
  185. ...obj,
  186. [country]: (allTime.country[country] || 0) + count
  187. }),
  188. {}
  189. )
  190. },
  191. referrer: {
  192. ...allTime.referrer,
  193. ...Object.entries(visit.referrers).reduce(
  194. (obj, [referrer, count]) => ({
  195. ...obj,
  196. [referrer]: (allTime.referrer[referrer] || 0) + count
  197. }),
  198. {}
  199. )
  200. }
  201. };
  202. stats.allTime.views[index] = view + visit.total;
  203. }
  204. const response: IGetStatsResponse = {
  205. allTime: {
  206. stats: utils.statsObjectToArray(stats.allTime.stats),
  207. views: stats.allTime.views
  208. },
  209. lastDay: {
  210. stats: utils.statsObjectToArray(stats.lastDay.stats),
  211. views: stats.lastDay.views
  212. },
  213. lastMonth: {
  214. stats: utils.statsObjectToArray(stats.lastMonth.stats),
  215. views: stats.lastMonth.views
  216. },
  217. lastWeek: {
  218. stats: utils.statsObjectToArray(stats.lastWeek.stats),
  219. views: stats.lastWeek.views
  220. },
  221. updatedAt: new Date().toISOString()
  222. };
  223. if (match.link_id) {
  224. const cacheTime = utils.getStatsCacheTime(total);
  225. const key = redis.key.stats(match.link_id);
  226. redis.set(key, JSON.stringify(response), "EX", cacheTime);
  227. }
  228. return response;
  229. };