visit.queries.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. const { isAfter, subDays, subHours, set, format } = require("date-fns");
  2. const utils = require("../utils");
  3. const redis = require("../redis");
  4. const knex = require("../knex");
  5. const env = require("../env");
  6. async function add(params) {
  7. const data = {
  8. ...params,
  9. country: params.country.toLowerCase(),
  10. referrer: params.referrer.toLowerCase()
  11. };
  12. const nowUTC = new Date().toISOString();
  13. const truncatedNow = nowUTC.substring(0, 10) + " " + nowUTC.substring(11, 14) + "00:00";
  14. return knex.transaction(async (trx) => {
  15. // Create a subquery first that truncates the
  16. const subquery = trx("visits")
  17. .select("visits.*")
  18. .select({
  19. created_at_hours: utils.knexUtils(trx).truncatedTimestamp("created_at", "hour")
  20. })
  21. .where({ link_id: params.id })
  22. .as("subquery");
  23. const visit = await trx
  24. .select("*")
  25. .from(subquery)
  26. .where("created_at_hours", "=", truncatedNow)
  27. .forUpdate()
  28. .first();
  29. if (visit) {
  30. const countries = typeof visit.countries === "string" ? JSON.parse(visit.countries) : visit.countries;
  31. const referrers = typeof visit.referrers === "string" ? JSON.parse(visit.referrers) : visit.referrers;
  32. await trx("visits")
  33. .where({ id: visit.id })
  34. .increment(`br_${data.browser}`, 1)
  35. .increment(`os_${data.os}`, 1)
  36. .increment("total", 1)
  37. .update({
  38. updated_at: utils.dateToUTC(new Date()),
  39. countries: JSON.stringify({
  40. ...countries,
  41. [data.country]: (countries[data.country] ?? 0) + 1
  42. }),
  43. referrers: JSON.stringify({
  44. ...referrers,
  45. [data.referrer]: (referrers[data.referrer] ?? 0) + 1
  46. })
  47. });
  48. } else {
  49. // This must also happen in the transaction to avoid concurrency
  50. await trx("visits").insert({
  51. [`br_${data.browser}`]: 1,
  52. countries: { [data.country]: 1 },
  53. referrers: { [data.referrer]: 1 },
  54. [`os_${data.os}`]: 1,
  55. total: 1,
  56. link_id: data.id
  57. });
  58. }
  59. return visit;
  60. });
  61. }
  62. async function find(match, total) {
  63. if (match.link_id && env.REDIS_ENABLED) {
  64. const key = redis.key.stats(match.link_id);
  65. const cached = await redis.client.get(key);
  66. if (cached) return JSON.parse(cached);
  67. }
  68. const stats = {
  69. lastDay: {
  70. stats: utils.getInitStats(),
  71. views: new Array(24).fill(0),
  72. total: 0
  73. },
  74. lastWeek: {
  75. stats: utils.getInitStats(),
  76. views: new Array(7).fill(0),
  77. total: 0
  78. },
  79. lastMonth: {
  80. stats: utils.getInitStats(),
  81. views: new Array(30).fill(0),
  82. total: 0
  83. },
  84. lastYear: {
  85. stats: utils.getInitStats(),
  86. views: new Array(12).fill(0),
  87. total: 0
  88. }
  89. };
  90. const visitsStream = knex("visits").where(match).stream();
  91. const now = new Date();
  92. const periods = utils.getStatsPeriods(now);
  93. for await (const visit of visitsStream) {
  94. periods.forEach(([type, fromDate]) => {
  95. const isIncluded = isAfter(utils.parseDatetime(visit.created_at), fromDate);
  96. if (!isIncluded) return;
  97. const diffFunction = utils.getDifferenceFunction(type);
  98. const diff = diffFunction(now, utils.parseDatetime(visit.created_at));
  99. const index = stats[type].views.length - diff - 1;
  100. const view = stats[type].views[index];
  101. const period = stats[type].stats;
  102. const countries = typeof visit.countries === "string" ? JSON.parse(visit.countries) : visit.countries;
  103. const referrers = typeof visit.referrers === "string" ? JSON.parse(visit.referrers) : visit.referrers;
  104. stats[type].stats = {
  105. browser: {
  106. chrome: period.browser.chrome + visit.br_chrome,
  107. edge: period.browser.edge + visit.br_edge,
  108. firefox: period.browser.firefox + visit.br_firefox,
  109. ie: period.browser.ie + visit.br_ie,
  110. opera: period.browser.opera + visit.br_opera,
  111. other: period.browser.other + visit.br_other,
  112. safari: period.browser.safari + visit.br_safari
  113. },
  114. os: {
  115. android: period.os.android + visit.os_android,
  116. ios: period.os.ios + visit.os_ios,
  117. linux: period.os.linux + visit.os_linux,
  118. macos: period.os.macos + visit.os_macos,
  119. other: period.os.other + visit.os_other,
  120. windows: period.os.windows + visit.os_windows
  121. },
  122. country: {
  123. ...period.country,
  124. ...Object.entries(countries).reduce(
  125. (obj, [country, count]) => ({
  126. ...obj,
  127. [country]: (period.country[country] || 0) + count
  128. }),
  129. {}
  130. )
  131. },
  132. referrer: {
  133. ...period.referrer,
  134. ...Object.entries(referrers).reduce(
  135. (obj, [referrer, count]) => ({
  136. ...obj,
  137. [referrer]: (period.referrer[referrer] || 0) + count
  138. }),
  139. {}
  140. )
  141. }
  142. };
  143. stats[type].views[index] += visit.total;
  144. stats[type].total += visit.total;
  145. });
  146. }
  147. const response = {
  148. lastYear: {
  149. stats: utils.statsObjectToArray(stats.lastYear.stats),
  150. views: stats.lastYear.views,
  151. total: stats.lastYear.total
  152. },
  153. lastDay: {
  154. stats: utils.statsObjectToArray(stats.lastDay.stats),
  155. views: stats.lastDay.views,
  156. total: stats.lastDay.total
  157. },
  158. lastMonth: {
  159. stats: utils.statsObjectToArray(stats.lastMonth.stats),
  160. views: stats.lastMonth.views,
  161. total: stats.lastMonth.total
  162. },
  163. lastWeek: {
  164. stats: utils.statsObjectToArray(stats.lastWeek.stats),
  165. views: stats.lastWeek.views,
  166. total: stats.lastWeek.total
  167. },
  168. updatedAt: new Date()
  169. };
  170. if (match.link_id && env.REDIS_ENABLED) {
  171. const key = redis.key.stats(match.link_id);
  172. redis.client.set(key, JSON.stringify(response), "EX", 60);
  173. }
  174. return response;
  175. };
  176. module.exports = {
  177. add,
  178. find
  179. };